Как теперь использовать контейнерные запросы

Недавно Крис Койер написал в блоге сообщение, в котором задал вопрос:

Теперь, когда контейнерные запросы поддерживаются всеми браузерными движками, почему их не использует больше разработчиков?

В сообщении Криса перечислен ряд потенциальных причин (например, недостаточная осведомленность, старые привычки трудно умирают), но есть одна конкретная причина, которая выделяется.

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

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

Прагматичный подход

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

Тогда возникает вопрос: насколько всеобъемлющим должен быть запасной вариант?

Как и в случае с любым откатом, задача состоит в том, чтобы найти хороший баланс между полезностью и производительностью. Для функций CSS часто невозможно поддерживать полный API ( почему бы не использовать полифилл ). Однако вы можете продвинуться довольно далеко, если определите основной набор функций, которые хотят использовать большинство разработчиков, а затем оптимизируете резервный вариант только для этих функций.

Но каков «основной набор функций», который нужен большинству разработчиков для запросов к контейнерам? Чтобы ответить на этот вопрос, подумайте, как большинство разработчиков в настоящее время создают адаптивные сайты с медиа-запросами.

Практически все современные системы проектирования и библиотеки компонентов стандартизированы по принципам «мобильность прежде всего», реализованным с использованием набора предопределенных точек останова (таких как SM , MD , LG , XL ). По умолчанию компоненты оптимизированы для хорошего отображения на маленьких экранах, а затем стили условно наслаиваются для поддержки фиксированного набора экранов большей ширины. (Примеры см. в документации Bootstrap и Tailwind .)

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

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

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

Как это работает

Шаг 1. Обновите стили компонентов, чтобы использовать правила @container вместо правил @media

На этом первом этапе определите все компоненты на вашем сайте, которые, по вашему мнению, выиграют от изменения размера на основе контейнера, а не на основе размера области просмотра.

Хорошая идея начать с одного или двух компонентов, чтобы увидеть, как работает эта стратегия, но если вы хотите преобразовать 100% своих компонентов в стиль на основе контейнера, это тоже нормально! Самое замечательное в этой стратегии то, что при необходимости вы можете применять ее постепенно.

После того как вы определили компоненты, которые хотите обновить, вам нужно будет изменить каждое правило @media в CSS этих компонентов на правило @container . Вы можете оставить условия размера одинаковыми.

Если ваш CSS уже использует набор предопределенных точек останова, вы можете продолжать использовать их точно так, как они определены. Если вы еще не используете предопределенные точки останова, вам нужно будет определить для них имена (на которые вы будете ссылаться позже в JavaScript, см. шаг 2 ).

Вот пример стилей для компонента .photo-gallery , который по умолчанию представляет собой один столбец, а затем обновляет свой стиль, чтобы он стал двумя и тремя столбцами в точках останова MD и XL (соответственно):

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;
}

/* Styles for the `MD` breakpoint */
@media (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Styles for the `XL` breakpoint */
@media (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

Чтобы изменить эти стили компонентов с использования правил @media на использование правил @container , выполните поиск и замену в своем коде:

/* Before: */
@media (min-width: 768px) { /* ... */ }
@media (min-width: 1280px) { /* ... */ }

/* After: */
@container (min-width: 768px) { /* ... */ }
@container (min-width: 1280px) { /* ... */ }

После того как вы обновили стили компонентов с правил @media на правила @container на основе точек останова, следующим шагом будет настройка элементов контейнера.

Шаг 2. Добавьте элементы контейнера в HTML.

На предыдущем шаге были определены стили компонентов, основанные на размере элемента контейнера. Следующий шаг — определить, какие элементы на вашей странице должны быть теми элементами-контейнерами, к размеру которых будут относиться правила @container .

Вы можете объявить любой элемент элементом-контейнером в CSS, установив для его свойства container-type значение size или inline-size . Если ваши правила контейнера основаны на ширине, то обычно вы хотите использовать inline-size .

Рассмотрим сайт со следующей базовой структурой HTML:

<body>
  <div class="sidebar">...</div>
  <div class="content">...</div>
</body>

Чтобы сделать элементы .sidebar и .content на этом сайте контейнерами , добавьте это правило в свой CSS:

.content, .sidebar {
  container-type: inline-size;
}

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

Однако для браузеров, которые не поддерживают запросы к контейнерам, необходимо проделать некоторую дополнительную работу.

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

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

Следующий код определяет повторно используемый элемент <responsive-container> , который автоматически отслеживает изменения размера и добавляет классы точек останова, на основе которых ваш CSS может стилизовать:

// A mapping of default breakpoint class names and min-width sizes.
// Redefine these as needed based on your site's design.
const defaultBreakpoints = {SM: 512, MD: 768, LG: 1024, XL: 1280};

// A resize observer that monitors size changes to all <responsive-container>
// elements and calls their `updateBreakpoints()` method with the updated size.
const ro = new ResizeObserver((entries) => {
  entries.forEach((e) => e.target.updateBreakpoints(e.contentRect));
});

class ResponsiveContainer extends HTMLElement {
  connectedCallback() {
    const bps = this.getAttribute('breakpoints');
    this.breakpoints = bps ? JSON.parse(bps) : defaultBreakpoints;
    this.name = this.getAttribute('name') || '';
    ro.observe(this);
  }
  disconnectedCallback() {
    ro.unobserve(this);
  }
  updateBreakpoints(contentRect) {
    for (const bp of Object.keys(this.breakpoints)) {
      const minWidth = this.breakpoints[bp];
      const className = this.name ? `${this.name}-${bp}` : bp;
      this.classList.toggle(className, contentRect.width >= minWidth);
    }
  }
}

self.customElements.define('responsive-container', ResponsiveContainer);

Этот код работает путем создания ResizeObserver , который автоматически прослушивает изменения размера любых элементов <responsive-container> в DOM. Если изменение размера соответствует одному из определенных размеров точки останова, то к элементу добавляется класс с этим именем точки останова (и удаляется, если условие больше не соответствует).

Например, если width элемента <responsive-container> находится в диапазоне от 768 до 1024 пикселей (на основе значений точки останова по умолчанию, установленных в коде), то будут добавлены классы SM и MD , например:

<responsive-container class="SM MD">...</responsive-container>

Эти классы позволяют определять резервные стили для браузеров, которые не поддерживают контейнерные запросы (см. шаг 3: добавление резервных стилей в CSS ).

Чтобы обновить предыдущий HTML-код для использования этого элемента контейнера, измените элементы <div> боковой панели и основного содержимого на элементы <responsive-container> :

<body>
  <responsive-container class="sidebar">...</responsive-container>
  <responsive-container class="content">...</responsive-container>
</body>

В большинстве ситуаций вы можете просто использовать элемент <responsive-container> без какой-либо настройки, но если вам все же нужно его настроить, доступны следующие параметры:

  • Пользовательские размеры точек останова. В этом коде используется набор имен классов точек останова по умолчанию и размеров минимальной ширины, но вы можете изменить эти значения по умолчанию на любые, какие захотите. Вы также можете переопределить эти значения для каждого элемента, используя атрибут breakpoints .
  • Именованные контейнеры. Этот код также поддерживает именованные контейнеры путем передачи атрибута name . Это может быть важно, если вам нужно вложить элементы контейнера. Более подробную информацию смотрите в разделе ограничений .

Вот пример, в котором устанавливаются оба этих параметра конфигурации:

<responsive-container
  name='sidebar'
  breakpoints='{"bp1":500,"bp2":1000,"bp3":1500}'>
</responsive-container>

Наконец, при объединении этого кода убедитесь, что вы используете обнаружение функций и динамический import() чтобы загружать его только в том случае, если браузер не поддерживает запросы к контейнеру.

if (!CSS.supports('container-type: inline-size')) {
  import('./path/to/responsive-container.js');
}

Шаг 3. Добавьте резервные стили в CSS.

Последним шагом в этой стратегии является добавление резервных стилей для браузеров, которые не распознают стили, определенные в правилах @container . Сделайте это, дублируя эти правила, используя классы точек останова, которые устанавливаются в элементах <responsive-container> .

Продолжая предыдущий пример .photo-gallery , резервные стили для двух правил @container могут выглядеть следующим образом:

/* Container query styles for the `MD` breakpoint. */
@container (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Fallback styles for the `MD` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.MD) .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Container query styles for the `XL` breakpoint. */
@container (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

/* Fallback styles for the `XL` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.XL) .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

В этом коде для каждого правила @container существует эквивалентное правило, условно соответствующее элементу <responsive-container> , если присутствует соответствующий класс точки останова.

Часть селектора, соответствующая элементу <responsive-container> , заключена в функциональный селектор псевдокласса :where() , чтобы сохранить специфичность резервного селектора эквивалентной специфичности исходного селектора в правиле @container .

Каждое резервное правило также заключено в объявление @supports . Хотя это не является строго необходимым для работы резервного варианта, это означает, что браузер полностью игнорирует эти правила, если он поддерживает запросы контейнера, что может улучшить производительность сопоставления стилей в целом. Это также потенциально позволяет инструментам сборки или CDN удалять эти объявления, если они знают, что браузер поддерживает запросы к контейнерам и не нуждается в этих резервных стилях.

Основным недостатком этой запасной стратегии является необходимость дважды повторять объявление стиля, что утомительно и чревато ошибками. Однако, если вы используете препроцессор CSS, вы можете абстрагировать его в миксин, который генерирует для вас как правило @container , так и резервный код. Вот пример использования Sass:

@use 'sass:map';

$breakpoints: (
  'SM': 512px,
  'MD': 576px,
  'LG': 1024px,
  'XL': 1280px,
);

@mixin breakpoint($breakpoint) {
  @container (min-width: #{map.get($breakpoints, $breakpoint)}) {
    @content();
  }
  @supports not (container-type: inline-size) {
    :where(responsive-container.#{$breakpoint}) & {
      @content();
    }
  }
}

Затем, получив этот миксин, вы можете обновить исходные стили компонента .photo-gallery примерно так, что полностью исключит дублирование:

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;

  @include breakpoint('MD') {
    grid-template-columns: 1fr 1fr;
  }

  @include breakpoint('XL') {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

И это все, что нужно!

Резюме

Итак, подведем итоги: как обновить свой код, чтобы теперь использовать контейнерные запросы с резервным вариантом для разных браузеров.

  1. Компоненты удостоверений, стиль которых вы хотите задать относительно их контейнера, и обновите правила @media в их CSS, чтобы использовать правила @container . Кроме того (если вы еще этого не сделали), стандартизируйте набор имен точек останова, чтобы они соответствовали условиям размера в правилах вашего контейнера.
  2. Добавьте JavaScript, который поддерживает пользовательский элемент <responsive-container> , а затем добавьте элемент <responsive-container> в любые области контента на вашей странице, относительно которых вы хотите, чтобы ваши компоненты были связаны.
  3. Для поддержки старых браузеров добавьте в CSS резервные стили, соответствующие классам точек останова, которые автоматически добавляются к элементам <responsive-container> в вашем HTML. В идеале используйте миксин препроцессора CSS, чтобы избежать необходимости писать одни и те же стили дважды.

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

Увидеть это в действии

Вероятно, лучший способ понять, как все эти шаги сочетаются друг с другом, — это увидеть их демонстрацию в действии .

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

Эта демонстрация представляет собой обновленную версию сайта, созданного в 2019 году (до появления контейнерных запросов), чтобы проиллюстрировать, почему контейнерные запросы необходимы для создания по-настоящему отзывчивых библиотек компонентов.

Поскольку на этом сайте уже были определены стили для множества «адаптивных компонентов», он был идеальным кандидатом для проверки представленной здесь стратегии на нетривиальном сайте. Оказывается, на самом деле его было довольно просто обновить и почти не потребовалось вносить изменения в исходные стили сайта.

Вы можете просмотреть полный исходный код демонстрационной версии на GitHub и обязательно обратить внимание на демонстрационный компонент CSS , чтобы увидеть, как определяются резервные стили. Если вы хотите протестировать только резервное поведение, существует резервная демонстрационная версия, включающая именно этот вариант — даже в браузерах, поддерживающих контейнерные запросы.

Ограничения и потенциальные улучшения

Как упоминалось в начале статьи, описанная здесь стратегия хорошо работает для большинства случаев использования, которые действительно интересуют разработчиков при выполнении запросов к контейнеру.

Тем не менее, есть несколько более сложных вариантов использования, которые эта стратегия намеренно не пытается поддерживать, и они рассматриваются ниже:

Единицы запроса контейнера

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

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

responsive-container {
  --cqw: 1cqw;
  --cqh: 1cqh;
}

И затем всякий раз, когда вам понадобится доступ к модулям запроса контейнера, используйте эти свойства, а не сам модуль:

.photo-gallery {
  font-size: calc(10 * var(--cqw));
}

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

class ResponsiveContainer extends HTMLElement {
  // ...
  updateBreakpoints(contentRect) {
    this.style.setProperty('--cqw', `${contentRect.width / 100}px`);
    this.style.setProperty('--cqh', `${contentRect.height / 100}px`);

    // ...
  }
}

Это эффективно позволяет вам «передавать» эти значения из JavaScript в CSS, а затем у вас есть все возможности CSS (например, calc() , min() , max() , clamp() ) для манипулирования ими по мере необходимости.

Поддержка логических свойств и режима записи.

Возможно, вы заметили использование inline-size а не width в объявлениях @container в некоторых из этих примеров CSS. Возможно, вы также заметили новые единицы измерения cqi и cqb (для строчных и блочных размеров соответственно). Эти новые функции отражают переход CSS к логическим свойствам и значениям , а не к физическим или направленным.

К сожалению, API-интерфейсы, такие как Resize Observer, по-прежнему сообщают значения width и height , поэтому, если вашим проектам нужна гибкость логических свойств, вам нужно выяснить это самостоятельно.

Хотя можно получить режим записи, используя что-то вроде getComputedStyle() , передавая элемент контейнера, это требует затрат , и на самом деле не существует хорошего способа определить, изменился ли режим записи.

По этой причине лучше всего, чтобы сам элемент <responsive-container> принимал свойство режима записи, которое владелец сайта может устанавливать (и обновлять) по мере необходимости. Чтобы реализовать это, вы должны следовать тому же подходу, который показан в предыдущем разделе, и менять местами width и height по мере необходимости.

Вложенные контейнеры

СвойствоContainer container-name позволяет присвоить контейнеру имя, на которое затем можно ссылаться в правиле @container . Именованные контейнеры полезны, если у вас есть контейнеры, вложенные в контейнеры, и вам нужны определенные правила, чтобы соответствовать только определенным контейнерам (а не только ближайшему контейнеру-предку).

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

Например, здесь есть два элемента <responsive-container> , обертывающие компонент .photo-gallery , но поскольку внешний контейнер больше внутреннего, к ним добавлены разные классы точек останова.

<responsive-container class="SM MD LG">
  ...
  <responsive-container class="SM">
    ...
    <div class="photo-gallery">...</div class="photo-gallery">
  </responsive-container>
</responsive-container>

В этом примере классы MD и LG во внешнем контейнере будут влиять на правила стиля, соответствующие компоненту .photo-gallery , который не соответствует поведению запросов контейнера (поскольку они соответствуют только ближайшему контейнеру-предку).

Чтобы справиться с этим, либо:

  1. Убедитесь, что вы всегда называете все вложенные контейнеры, а затем убедитесь, что ваши классы точек останова имеют префикс этого имени контейнера, чтобы избежать конфликтов.
  2. Используйте дочерний комбинатор вместо дочернего комбинатора в резервных селекторах (что немного более ограничивает).

В разделе вложенных контейнеров на демонстрационном сайте приведен пример этой работы с использованием именованных контейнеров, а также примеси Sass, которую он использует в коде для генерации резервных стилей как для именованных, так и для неименованных правил @container .

А как насчет браузеров, которые не поддерживают :where() , Custom Elements или Resize Observer?

Хотя эти API могут показаться относительно новыми, все они поддерживаются во всех браузерах уже более трех лет и являются частью широко доступной версии Baseline .

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

Даже в этом конкретном случае худшее, что может случиться, — это то, что резервный вариант не будет работать для очень небольшого процента ваших пользователей, а это означает, что они увидят представление по умолчанию, а не представление, оптимизированное для размера контейнера.

Функционал сайта по-прежнему должен работать, и это действительно важно.

Почему бы просто не использовать полифил запроса контейнера?

Функции CSS , как известно, сложно полифиллить , и обычно они требуют повторной реализации всего парсера CSS браузера и каскадной логики в JavaScript. В результате авторам полифилов CSS приходится идти на множество компромиссов, которые почти всегда сопровождаются многочисленными ограничениями функций, а также значительными издержками производительности.

По этим причинам мы обычно не рекомендуем использовать полифилы CSS в рабочей среде, включая полифил контейнера-запроса из Google Chrome Labs, который больше не поддерживается (и в первую очередь предназначался для демонстрационных целей).

Обсуждаемая здесь резервная стратегия имеет меньше ограничений, требует гораздо меньше кода и будет работать значительно лучше, чем любой полифил запроса контейнера.

Вам вообще нужно реализовать резервный вариант для старых браузеров?

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

По данным caniuse.com , контейнерные запросы поддерживают 90% пользователей Интернета по всему миру, и для многих людей, читающих этот пост, это число, вероятно, немного выше для их пользовательской базы. Поэтому важно помнить, что большинство ваших пользователей увидят версию вашего пользовательского интерфейса для контейнерных запросов. И для 10% пользователей, которые этого не сделают, это не значит, что у них будет испорченный опыт. При использовании этой стратегии в худшем случае эти пользователи увидят макет по умолчанию или «мобильный» макет для некоторых компонентов, что не является концом света.

При выборе компромисса рекомендуется оптимизировать работу для большинства пользователей, а не использовать по умолчанию подход с наименьшим общим знаменателем, который дает всем пользователям единообразный, но некачественный опыт.

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

С нетерпением жду

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

Хотя описанная здесь стратегия требует некоторой дополнительной работы, она должна быть достаточно простой и понятной, чтобы большинство людей могли использовать ее на своих сайтах. Тем не менее, безусловно, есть возможности сделать его еще проще. Одной из идей было бы объединить множество разрозненных частей в один компонент, оптимизированный для конкретной платформы или стека, который выполнит всю работу за вас. Если вы создадите что-то подобное, сообщите нам, и мы поможем это продвинуть!

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