Запросы CSS-контейнеров: варианты использования и стратегии миграции
Опубликовано: 2022-03-10Когда мы пишем медиа-запросы для элемента пользовательского интерфейса, мы всегда описываем, как оформляется этот элемент в зависимости от размеров экрана. Этот подход хорошо работает, когда скорость отклика медиазапроса целевого элемента должна зависеть только от размера области просмотра. Давайте взглянем на следующий пример адаптивного макета страницы.
Однако адаптивный веб-дизайн (RWD) не ограничивается макетом страницы — отдельные компоненты пользовательского интерфейса обычно имеют медиа-запросы, которые могут менять свой стиль в зависимости от размеров области просмотра.
Возможно, вы уже заметили проблему с предыдущим утверждением — макет отдельного компонента пользовательского интерфейса часто не зависит исключительно от размеров области просмотра. В то время как макет страницы — это элемент, тесно связанный с размерами области просмотра и являющийся одним из самых важных элементов в HTML, компоненты пользовательского интерфейса могут использоваться в различных контекстах и контейнерах. Если подумать, область просмотра — это просто контейнер, а компоненты пользовательского интерфейса могут быть вложены в другие контейнеры со стилями, влияющими на размеры и макет компонента.
Несмотря на то, что один и тот же компонент карточки продукта используется как в верхней, так и в нижней части, стили компонента зависят не только от размеров окна просмотра, но также от контекста и свойств CSS контейнера (например, сетки в примере), где он размещен.
Конечно, мы можем структурировать наш CSS, чтобы мы поддерживали вариации стиля для разных контекстов и контейнеров, чтобы вручную решить проблему макета. В худшем случае этот вариант будет добавлен с переопределением стиля, что приведет к дублированию кода и проблемам специфичности.
.product-card { /* Default card style */ } .product-card--narrow { /* Style variation for narrow viewport and containers */ } @media screen and (min-width: 569px) { .product-card--wide { /* Style variation for wider viewport and containers */ } }
Однако это скорее обходной путь для ограничений медиа-запросов, чем правильное решение. При написании медиа-запросов для элементов пользовательского интерфейса мы пытаемся найти «волшебное» значение области просмотра для точки останова, когда целевой элемент имеет минимальные размеры, при которых макет не ломается. Короче говоря, мы связываем «волшебное» значение размера области просмотра со значением размеров элемента . Это значение обычно отличается от размера области просмотра и подвержено ошибкам при изменении внутренних размеров контейнера или макета.
Следующий пример демонстрирует именно эту проблему — несмотря на то, что адаптивный элемент карточки продукта был реализован и выглядит хорошо в стандартном варианте использования, он выглядит сломанным, если он перемещен в другой контейнер со свойствами CSS, влияющими на размеры элемента. Каждый дополнительный вариант использования требует добавления дополнительного кода CSS , что может привести к дублированию кода, раздуванию кода и коду, который трудно поддерживать.
Это одна из проблем, которые пытаются исправить запросы контейнера. Контейнерные запросы расширяют существующие функциональные возможности медиа-запросов за счет запросов, зависящих от размеров целевого элемента. Есть три основных преимущества использования этого подхода:
- Стили запроса контейнера применяются в зависимости от размеров самого целевого элемента. Компоненты пользовательского интерфейса смогут адаптироваться к любому заданному контексту или контейнеру.
- Разработчикам не нужно будет искать значение измерения области просмотра «магическое число», которое связывает медиа-запрос области просмотра с целевым измерением компонента пользовательского интерфейса в конкретном контейнере или конкретном контексте.
- Нет необходимости добавлять дополнительные классы CSS или медиа-запросы для разных контекстов и вариантов использования.
«Идеальный адаптивный веб-сайт — это система гибких модульных компонентов, которые можно переназначить для использования в различных контекстах».
— «Контейнерные запросы: еще раз к взлому», Мэт Маркиз.
Прежде чем мы углубимся в контейнерные запросы, нам нужно проверить поддержку браузера и посмотреть, как мы можем включить экспериментальную функцию в нашем браузере.
Поддержка браузера
Контейнерные запросы — это экспериментальная функция, доступная в настоящее время в версии Chrome Canary на момент написания этой статьи. Если вы хотите следовать и запускать примеры CodePen в этой статье, вам нужно включить контейнерные запросы в следующем URL-адресе настроек.
chrome://flags/#enable-container-queries
Если вы используете браузер, который не поддерживает контейнерные запросы, изображение, демонстрирующее предполагаемый рабочий пример, будет предоставлено вместе с демонстрацией CodePen.
Работа с контейнерными запросами
Контейнерные запросы не так просты, как обычные медиа-запросы. Нам придется добавить дополнительную строку кода CSS в наш элемент пользовательского интерфейса, чтобы заставить контейнерные запросы работать, но для этого есть причина, и мы рассмотрим ее далее.
Защитное свойство
Свойство CSS contain
было добавлено в большинство современных браузеров и на момент написания этой статьи поддерживается 75% браузеров. Свойство contain
в основном используется для оптимизации производительности, подсказывая браузеру, какие части (поддеревья) страницы можно рассматривать как независимые и не повлияют на изменения в других элементах дерева. Таким образом, если изменение произойдет в одном элементе, браузер повторно отобразит только эту часть (поддерево), а не всю страницу. Со значениями свойства contains мы можем указать, какие типы contain
мы хотим использовать — layout
, size
или paint
.
Есть contain
замечательных статей о свойстве contains, в которых более подробно описываются доступные параметры и варианты использования, поэтому я сосредоточусь только на свойствах, связанных с запросами к контейнеру.
Какое отношение свойство содержимого CSS, используемое для оптимизации, имеет к запросам контейнера? Чтобы контейнерные запросы работали, браузеру необходимо знать, происходит ли изменение в макете дочерних элементов, что он должен повторно отображать только этот компонент. Браузер будет знать, что нужно применить код в запросе контейнера к соответствующему компоненту, когда компонент визуализируется или изменяется размер компонента.
Мы будем использовать значения layout
style
для contain
, но нам также потребуется дополнительное значение, которое сигнализирует браузеру об оси, в которой произойдет изменение.
-
inline-size
Сдерживание на встроенной оси. Ожидается, что это значение будет иметь значительно больше вариантов использования, поэтому оно реализуется в первую очередь. -
block-size
Сдерживание на оси блока. Он все еще находится в разработке и в настоящее время недоступен.
Один незначительный недостаток свойства contains contain
в том, что наш элемент макета должен быть дочерним элементом contain
элемента, а это означает, что мы добавляем дополнительный уровень вложенности.
<section> <article class="card"> <div class="card__wrapper"> <!-- Card content --> </div> </article> </section>
.card { contain: layout inline-size style; } .card__wrapper { display: grid; grid-gap: 1.5em; grid-template-rows: auto auto; /* ... */ }
Обратите внимание, что мы не добавляем это значение в более удаленный родительский section
и сохраняем контейнер как можно ближе к затронутому элементу.
«Производительность — это искусство избегать работы и делать любую работу максимально эффективной. Во многих случаях речь идет о работе с браузером, а не против него».
— «Производительность рендеринга», Пол Льюис.
Именно поэтому мы должны корректно сигнализировать браузеру об изменении. Обертывание удаленного родительского элемента свойством contain
может привести к обратным результатам и отрицательно сказаться на производительности страницы. В наихудших сценариях неправильного использования свойства contain
макет может даже сломаться, и браузер не отобразит его правильно.
Запрос контейнера
После того, как в оболочку элемента карты было добавлено свойство contain
, мы можем написать контейнерный запрос. Мы contain
свойство содержимого к элементу с классом card
, так что теперь мы можем включить любой из его дочерних элементов в запрос контейнера.
Как и в случае с обычными медиа-запросами, нам нужно определить запрос, используя свойства min-width
или max-width
, и вложить все селекторы внутрь блока. Однако мы будем использовать ключевое слово @container
вместо @media
для определения контейнерного запроса.
@container (min-width: 568px) { .card__wrapper { align-items: center; grid-gap: 1.5em; grid-template-rows: auto; grid-template-columns: 150px auto; } .card__image { min-width: auto; height: auto; } }
И card__wrapper
, и элемент card__image
являются дочерними элементами элемента card
, для которого contain
свойство содержимого. Когда мы заменяем обычные медиазапросы контейнерными запросами, удаляем дополнительные классы CSS для узких контейнеров и запускаем пример CodePen в браузере, который поддерживает контейнерные запросы, мы получаем следующий результат.
Обратите внимание, что контейнерные запросы в настоящее время не отображаются в инструментах разработчика Chrome , что немного усложняет отладку контейнерных запросов. Ожидается, что в будущем в браузер будет добавлена надлежащая поддержка отладки.
Вы можете видеть, как контейнерные запросы позволяют нам создавать более надежные и многократно используемые компоненты пользовательского интерфейса, которые можно адаптировать практически к любому контейнеру и макету. Однако до надлежащей поддержки контейнерных запросов браузерами еще далеко. Давайте попробуем и посмотрим, сможем ли мы реализовать контейнерные запросы, используя прогрессивное улучшение.
Прогрессивное улучшение и полифиллы
Давайте посмотрим, сможем ли мы добавить запасной вариант к варианту класса CSS и медиа-запросам. Мы можем использовать запросы функций CSS с правилом @supports
для обнаружения доступных функций браузера. Однако мы не можем проверять другие запросы, поэтому нам нужно добавить проверку для значения contain: layout inline-size style
. Придется предположить, что браузеры, поддерживающие свойство inline-size
также поддерживают контейнерные запросы.
/* Check if the inline-size value is supported */ @supports (contain: inline-size) { .card { contain: layout inline-size style; } } /* If the inline-size value is not supported, use media query fallback */ @supports not (contain: inline-size) { @media (min-width: 568px) { /* ... */ } } /* Browser ignores @container if it's not supported */ @container (min-width: 568px) { /* Container query styles */ }
Однако такой подход может привести к дублированию стилей, поскольку одни и те же стили применяются как запросом контейнера, так и запросом мультимедиа. Если вы решите реализовать контейнерные запросы с прогрессивным улучшением, вы захотите использовать препроцессор CSS, такой как SASS, или постпроцессор, такой как PostCSS, чтобы избежать дублирования блоков кода и вместо этого использовать миксины CSS или другой подход.
Поскольку эта спецификация запроса контейнера все еще находится на экспериментальной стадии, важно помнить, что спецификация или реализация могут измениться в будущих выпусках.
Кроме того, вы можете использовать полифиллы, чтобы обеспечить надежный запасной вариант. Я хотел бы выделить два полифилла JavaScript, которые в настоящее время активно поддерживаются и предоставляют необходимые функции контейнерных запросов:
-
cqfill
Джонатан Нил
Полифил JavaScript для CSS и PostCSS -
react-container-query
Крис Гарсия
Пользовательский хук и компонент для React
Переход от медиазапросов к контейнерным запросам
Если вы решите внедрить контейнерные запросы в существующий проект, в котором используются мультимедийные запросы, вам потребуется провести рефакторинг кода HTML и CSS. Я обнаружил, что это самый быстрый и простой способ добавления контейнерных запросов, в то же время обеспечивающий надежный запасной вариант для медиа-запросов. Давайте посмотрим на предыдущий пример карты.
<section> <div class="card__wrapper card__wrapper--wide"> <!-- Wide card content --> </div> </section> /* ... */ <aside> <div class="card__wrapper"> <!-- Narrow card content --> </div> </aside>
.card__wrapper { display: grid; grid-gap: 1.5em; grid-template-rows: auto auto; /* ... */ } .card__image { /* ... */ } @media screen and (min-width: 568px) { .card__wrapper--wide { align-items: center; grid-gap: 1.5em; grid-template-rows: auto; grid-template-columns: 150px auto; } .card__image { /* ... */ } }
Во-первых, оберните корневой HTML-элемент, к которому применен медиа-запрос, элементом, имеющим свойство contain
.
<section> <article class="card"> <div class="card__wrapper"> <!-- Card content --> </div> </article> </section>
@supports (contain: inline-size) { .card { contain: layout inline-size style; } }
Затем оберните медиа-запрос в запрос функции и добавьте запрос-контейнер.
@supports not (contain: inline-size) { @media (min-width: 568px) { .card__wrapper--wide { /* ... */ } .card__image { /* ... */ } } } @container (min-width: 568px) { .card__wrapper { /* Same code as .card__wrapper--wide in media query */ } .card__image { /* Same code as .card__image in media query */ } }
Хотя этот метод приводит к некоторому раздуванию кода и дублированию кода, с помощью SASS или PostCSS вы можете избежать дублирования кода разработки, поэтому исходный код CSS остается пригодным для сопровождения.
Как только контейнерные запросы получат надлежащую поддержку браузера, вы можете рассмотреть возможность удаления @supports not (contain: inline-size)
и продолжить поддержку исключительно контейнерных запросов.
Стефани Эклс недавно опубликовала замечательную статью о контейнерных запросах, описывающую различные стратегии миграции. Рекомендую ознакомиться с дополнительной информацией по теме.
Сценарии использования
Как мы видели из предыдущих примеров, контейнерные запросы лучше всего использовать для многократно используемых компонентов с макетом, который зависит от доступного пространства контейнера и который можно использовать в различных контекстах и добавлять в разные контейнеры на странице.
Другие примеры включают (примеры требуют браузера, поддерживающего контейнерные запросы):
- Модульные компоненты, такие как карточки, элементы форм, баннеры и т. д.
- Адаптируемые макеты
- Разбивка на страницы с различными функциями для мобильных и настольных компьютеров
- Увлекательные эксперименты с изменением размера CSS
Заключение
Как только спецификация будет реализована и будет широко поддерживаться в браузерах, контейнерные запросы могут стать функцией, меняющей правила игры. Это позволит разработчикам писать запросы на уровне компонентов, перемещая запросы ближе к связанным компонентам, вместо того, чтобы использовать удаленные и едва связанные медиа-запросы окна просмотра. Это приведет к более надежным, повторно используемым и ремонтопригодным компонентам, которые смогут адаптироваться к различным вариантам использования, макетам и контейнерам.
В настоящее время контейнерные запросы все еще находятся на ранней экспериментальной стадии, и реализация может измениться. Если вы хотите начать использовать контейнерные запросы в своих проектах уже сегодня, вам нужно добавить их с помощью прогрессивного улучшения с обнаружением функций или использовать полифилл JavaScript. Оба случая приведут к некоторым накладным расходам в коде, поэтому, если вы решите использовать контейнерные запросы на этом раннем этапе, обязательно запланируйте рефакторинг кода после того, как функция станет широко поддерживаться.
использованная литература
- «Контейнерные запросы: краткое руководство», Дэвид А. Херрон
- «Поздоровайтесь с контейнерными запросами CSS», Ахмад Шадид
- «Контейнер CSS в Chrome 52», Пол Льюис.
- «Помощь браузерам в оптимизации с помощью свойства CSS Contain», Рэйчел Эндрю.