Как фронтенд-дизайнер я за последние 6 лет не был так взволнован новой CSS-функцией, как сейчас. Благодаря усилиям Мириам Сюзанны и других умных людей прототип контейнерных запросов можно включить в Chrome Canary

О контейнерных запросах запомнилось много шуток, но они, наконец, здесь. В этой статье я расскажу, зачем нам нужны контейнерные запросы, как они облегчат нашу жизнь, и, самое важное, ваши компоненты и макеты станут мощнее. Если вы взволновались так же, как и я, давайте углубимся в тему. Вы готовы?


Проблема с медиазапросами

Веб-страница состоит из разных разделов и компонентов, которые мы делаем отзывчивыми при помощи медиазапросов CSS. В этом нет ничего плохого, но есть ограничения. Например, мы можем написать медиазапрос, чтобы показать минимальную версию компонента на мобильном или настольном компьютере. Часто отзывчивый веб-дизайн не имеет отношения к видовому экрану или к размеру экрана. Он касается размера контейнера. Рассмотрим такой пример:

Вот очень типичный макет с компонентом — карточкой. И два варианта:

  • Стопкой (смотрите aside).

  • Горизонтально (смотрите main).

Реализовать такое на CSS можно несколькими способами, вот самый распространённый: нам нужно создать базовый компонент, а затем написать его вариации.

.c-article {
  /* The default, stacked version */
}

.c-article > * + * {
  margin-top: 1rem;
}

/* The horizontal version */
@media (min-width: 46rem) {
  .c-article--horizontal {
    display: flex;
    flex-wrap: wrap;
  }

  .c-article > * + * {
    margin-top: 0;
  }

  .c-article__thumb {
    margin-right: 1rem;
  }
}

Обратите внимание, что мы описали класс .c-article--horizontal для работы с горизонтальной версией компонента. Если ширина видового экрана больше 46rem, компонент должен переключаться на горизонтальную версию. Это не плохо, но каким-то образом заставляет ощущать себя ограниченным. Хочется, чтобы компонент реагировал на ширину своего родительского компонента, а не на видовой экран браузера или размер экрана.

Считайте, что мы хотим использовать стандартную .c-c-card в разделе main. Что произойдёт? Ну, карта расширится до ширины своего родителя и, следовательно, окажется слишком большой. Посмотрите на рисунок:

Это проблема, и мы можем решить её при помощи контейнерных запросов. (Есть! наконец-то!) До погружения в тему давайте взглянем на результат.

Нам нужно сообщить компоненту, что, если непосредственная ширина родительского элемента больше 400 пикселей, ему нужно переключиться на горизонтальный стиль. CSS будет выглядеть так:

<div class="o-grid">
  <div class="o-grid__item">
    <article class="c-article">
      <!-- content -->
    </article>
  </div>
  <div class="o-grid__item">
    <article class="c-article">
      <!-- content -->
    </article>
  </div>
</div>
.o-grid__item {
  contain: layout inline-size;
}

.c-article {
  /* The default style */
}

@container (min-width: 400px) {
  .c-article {
    /* The styles that will make the article horizontal**
        ** instead of a card style.. */
  }
}

Как помогут контейнерные запросы?

Предупреждение: контейнерные запросы CSS пока поддерживаются только в Chrome Canary с экспериментальным флагом.

При помощи контейнерных запросов CSS  мы можем решить вышеуказанную проблему и создать растягиваемый компонент. Это означает, что мы можем вложить компонент в узкий родительский элемент, и он превратится в версию "стопкой", а в широком родительском компоненте — в горизонтальную версию. Опять же все элементы не зависят от ширины видового экрана.

Вот как я представляю себе это:

Фиолетовый контур — это ширина родительского компонента. Обратите внимание, как компонент адаптируется к большему размеру своего родительского компонента. Разве это не потрясающе? Вот мощь контейнерных запросов CSS.

Как работают контейнерные запросы

С контейнерными запросами теперь можно поэкспериментировать в Chrome Canary. Чтобы включить их, перейдите в chrome://flags, найдите чекбокс "container queries" и отметьте его.

Сначала добавим свойство contain. Компонент будет адаптироваться под родительскую ширину, поэтому нужно сказать браузеру, чтобы он перерисовал не всю страницу, а только её изменяемую область. Заранее сообщить об этом браузеру мы можем при помощи свойства contain.

Значение inline-size означает, что компонент реагирует только на изменения ширины родительского элемента [прим. перев. — в случае языков с вертикальным направлением, возможно, речь идёт о высоте]. Я попытался задействовать block-size, но это свойство ещё не работает. Пожалуйста, поправьте меня, если я ошибаюсь.

<div class="o-grid">
  <div class="o-grid__item">
    <article class="c-article">
      <!-- content -->
    </article>
  </div>
  <div class="o-grid__item">
    <article class="c-article">
      <!-- content -->
    </article>
  </div>
  <!-- other articles.. -->
</div>
.o-grid__item {
  contain: layout inline-size;
}

Это первый шаг. Мы определили элемент .o-grid__item как родительский для .c-article. Следующий шаг — добавить желаемые стили, чтобы контейнерные запросы работали.

.o-grid__item {
  contain: layout inline-size;
}

@container (min-width: 400px) {
  .c-article {
    display: flex;
    flex-wrap: wrap;
  }

  /* other CSS.. */
}

@container — это элемент .o-grid__item, а min-width: 400px — его ширина. Мы даже можем пойти дальше и добавить больше стилей. На видео показано, чего можно добиться от компонентов:

У нас есть следующие стили:

  1. По умолчанию (вид карточки).

  2. Горизонтальная карточка с маленьким предпросмотром.

  3. Горизонтальная карточка с большим предпросмотром.

  4. Если родительский компонент слишком большой, стиль будет похож на стиль раздела hero, указывая, что это избранная статья.

Давайте углубимся в примеры применения контейнерных запросов.

Случаи применения контейнерных запросов CSS

Контейнерные запросы и CSS-грид с auto-fit

В некоторых случаях применение auto-fit в CSS-гриде приводит к неожиданным результатам. Например, компонент оказывается слишком широким, и его содержимое трудно читается. Дам немного контекста: вот визуальный элемент, показывающий разницу между auto-fit и auto-fill в гриде CSS:

Обратите внимание, что при использовании auto-fit элементы расширяются, чтобы заполнить доступное пространство. Однако в случае автоматического заполнения `` элементы грида не будут разрастаться, вместо этого у нас будет свободное пространство (пунктирный элемент в крайнем правом углу).

Возможно, вы сейчас думаете, как этот факт относится к контейнерным запросам. Каждый элемент грида — это контейнер, и, когда он расширяется (то есть когда мы используем auto-fit), нам нужно, чтобы компонент изменился, опираясь на это расширение.

<div class="o-grid">
  <div class="o-grid__item">
    <article class="c-article"></article>
  </div>
  <div class="o-grid__item">
    <article class="c-article"></article>
  </div>
  <div class="o-grid__item">
    <article class="c-article"></article>
  </div>
  <div class="o-grid__item">
    <article class="c-article"></article>
  </div>
</div>
.o-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  grid-gap: 1rem;
}

Когда элемента четыре, результат должен выглядеть примерно так:

Что произойдёт, когда количество статей уменьшится? Чем меньше у нас будет элементов, тем они будут шире. Так происходит потому, что мы используем auto-fit. Первый выглядит хорошо, но последние два (2 на ряд, 1 на ряд) — не очень: они слишком широкие:

Что делать, если каждый компонент статьи изменяется в зависимости от ширины родительского компонента? Если так, auto-fit будет очень хорошим преимуществом. Вот что нужно сделать: если ширина элемента грида превышает 400 пикселей, статья должна переключиться на горизонтальный стиль, а добиться этого можно так:

.o-grid__item {
  contain: layout inline-size;
}

@container (min-width: 400px) {
  .c-article {
    display: flex;
    flex-wrap: wrap;
  }
}

Кроме того, если статья — единственный элемент в гриде, хочется отобразить её с разделом hero.

.o-grid__item {
  contain: layout inline-size;
}

@container (min-width: 700px) {
  .c-article {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 350px;
  }

  .card__thumb {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

Вот и всё. У нас есть компонент, реагирующий на ширину родительского компонента, и он работает в любом контексте. Разве это не потрясающе? Посмотрите демо на CodePen.

aside и main

Часто нам нужно настроить компонент, чтобы он работал в контейнерах небольшой ширины, таких как <aside>. Идеальный пример — раздел новостей. Когда ширина маленькая, нужно, чтобы её элементы складывались, а когда места достаточно, нужно горизонтальное расположение элементов.

Как видно на рисунке, мы работаем с разделом новостей в двух разных контекстах:

  • Раздел aside.

  • Раздел main.

Такое невозможно без контейнерных запросов, если у нас нет класса вариаций в CSS, например .newsletter--stacked или чего-то в этом роде.

Я знаю, что мы можем сказать браузеру, чтобы элементы были обтекающими в случае, когда flexbox не хватает пространства, но этого недостаточно. Мне нужно гораздо больше контроля, чтобы делать кнопку во всю ширину и прятать определённые элементы:

.newsletter-wrapper {
  contain: layout inline-size;
}

/* The default, stacked version */
.newsletter {
  /* CSS styles */
}

.newsletter__title {
  font-size: 1rem;
}

.newsletter__desc {
  display: none;
}

/* The horizontal version */
@container (min-width: 600px) {
  .newsletter {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .newsletter__title {
    font-size: 1.5rem;
  }

  .newsletter__desc {
    display: block;
  }
}

Вот видео с результатом.

Посмотрите демоверсию на CodePen.

Пагинация

Я обнаружил, что контейнерные запросы хорошо подходят для пагинации. Сначала у нас могут быть кнопки “Previous” и “Next”; если пространства достаточно, можно скрыть кнопки и показать всю нумерацию страниц. Посмотрим на рисунок ниже:

Чтобы справиться с приведёнными выше состояниями, сначала нужно поработать над стилем по умолчанию (кнопками в виде стопки), а затем — над двумя другими состояниями.

.wrapper {
  contain: layout inline-size;
}

@container (min-width: 250px) {
  .pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .pagination li:not(:last-child) {
    margin-bottom: 0;
  }
}

@container (min-width: 500px) {
  .pagination {
    justify-content: center;
  }

  .pagination__item:not(.btn) {
    display: block;
  }

  .pagination__item.btn {
    display: none;
  }
}

Посмотрите демоверсию на CodePen.

Карточка профиля

Вот ещё один пример применения: контейнерные запросы идеально работают в нескольких контекстах. Состояние малого размера работает при маленьких видовых экранах и в контекстах вроде сайдбара. Состояние большого размера работает при гораздо больших контекстах, таких как размещение в гриде из двух колонок.

.p-card-wrapper {
  contain: layout inline-size;
}

.p-card {
  /* Default styles */
}

@container (min-width: 450px) {
  .meta {
    display: flex;
    justify-content: center;
    gap: 2rem;
    border-top: 1px solid #e8e8e8;
    background-color: #f9f9f9;
    padding: 1.5rem 1rem;
    margin: 1rem -1rem -1rem;
  }

  /* and other styles */
}

При помощи этого кода теперь мы видим, как компоненты работают в разных контекстах без единого медиазапроса.

Посмотрите демоверсию на CodePen.

Элементы формы

Я ещё не углублялся в случаи применения контейнерных запросов для форм, но в голове держу переключение с горизонтальных меток на метки в стопку.

.form-item {
  contain: layout inline-size;
}

.input-group {
  @container (min-width: 350px) {
    display: flex;
    align-items: center;
    gap: 1.5rem;

    input {
      flex: 1;
    }
  }
}

Посмотрите демо на CodePen.

Тестирование компонентов

Теперь, когда мы рассмотрели несколько случаев, когда контейнерные запросы могут быть полезны, возникает вопрос: как протестировать компонент? К счастью, мы можем сделать это с помощью свойства родительского компонента resize.

.parent {
  contain: layout inline-size;
  resize: horizontal;
  overflow: auto;
}

Об этом методе я узнал из этой замечательной статьи Брамуса Ван Дамма.

Легко ли отлаживать контейнерные запросы в DevTools?

Короткий ответ — нет. Вы не увидите чего-то вроде @container (min-width: value). Я думаю, что это дело времени, поддержка такой отладки появится.

А запасной вариант для браузеров без контейнерных запросов?

Да! Конечно. Определённым образом можно предоставить альтернативу. Вот две замечательные статьи с объяснением, как это сделать:

Заключение

Мне понравилось изучать контейнерные запросы в CSS и играть с ними в браузере. Я знаю, что они ещё официально не поддерживаются, но сейчас для этого самое время. Часть нашей работы как фронтендеров заключается в том, чтобы тестировать функции браузеров и помогать работающим над их реализацией людям. Чем лучше протестирована экспериментальная функциональность, тем меньше проблем мы увидим, когда она станет поддерживаться во всех основных браузерах.

Если вы уже имеете некоторые навыки работы с CSS, но всё ещё не можете сказать, что разбираетесь во фронтенде — можете обратить внимание на наш курс-профессию Frontend-разработчик, где вы сможете научиться создавать адаптивные веб-сайты с использованием CSS, Flexbox, разрабатывать интерактивные веб-сайты и приложения на JS и HTML, а также писать сложные компоненты на React и интерфейсы с авторизацией и с подключением к бэкенду.

Ну а если фронтенд для вас давно родная стихия — пора смотреть в сторону Fullstack-разработчика, настоящего универсала веб-разработки, которому будут рады в любом крупном проекте.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы