Внутри полифила запроса контейнера

Джеральд Монако
Gerald Monaco

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

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

Под капотом

Транспиляция

Когда парсер CSS внутри браузера встречает неизвестное at-правило, например новое правило @container , он просто отбрасывает его, как будто его никогда не существовало. Поэтому первое и самое важное, что должен сделать полифил, — это транспилировать запрос @container во что-то, что не будет отброшено.

Первым шагом транспиляции является преобразование правила @container верхнего уровня в запрос @media . В основном это гарантирует, что контент остается сгруппированным. Например, при использовании API CSSOM и при просмотре источника CSS.

До
@container (width > 300px) {
  /* content */
}
После
@media all {
  /* content */
}

До появления контейнерных запросов у автора CSS не было возможности произвольно включать или отключать группы правил. Для полифилла этого поведения необходимо также преобразовать правила внутри запроса контейнера. Каждому @container присваивается собственный уникальный идентификатор (например, 123 ), который используется для преобразования каждого селектора таким образом, чтобы он применялся только в том случае, если элемент имеет атрибут cq-XYZ включающий этот идентификатор. Этот атрибут будет установлен полифилом во время выполнения.

До
@container (width > 300px) {
  .card {
    /* ... */
  }
}
После
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

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

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

Учитывая этот CSS, элемент с классом .card всегда должен иметь color: red , поскольку более позднее правило всегда будет переопределять предыдущее правило с тем же селектором и спецификой. Таким образом, транспиляция первого правила и включение дополнительного селектора атрибутов без :where(...) увеличит специфичность и приведет к ошибочному применению color: blue .

Однако псевдокласс :where(...) довольно новый . Для браузеров, которые его не поддерживают, полифилл обеспечивает безопасный и простой обходной путь: вы можете намеренно повысить специфичность ваших правил, вручную добавив фиктивный селектор :not(.container-query-polyfill) к вашим правилам @container :

До
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
После
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

Это имеет ряд преимуществ:

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

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

Псевдоэлементы

Вы можете задать себе один вопрос: если полифил устанавливает для элемента некоторый атрибут cq-XYZ , включающий уникальный идентификатор контейнера 123 , как можно поддерживать псевдоэлементы, для которых не могут быть установлены атрибуты?

Псевдоэлементы всегда привязаны к реальному элементу в DOM, называемому исходным элементом . Во время транспиляции вместо этого к этому реальному элементу применяется условный селектор:

До
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
После
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

Вместо преобразования в #foo::before:where([cq-XYZ~="123"]) (что было бы недопустимо), условный селектор перемещается в конец исходного элемента #foo .

Однако это еще не все, что нужно. Контейнеру не разрешено изменять что-либо, что не содержится в нем (и контейнер не может находиться внутри самого себя), но учтите, что именно это произошло бы, если бы #foo сам был запрашиваемым элементом контейнера. Атрибут #foo[cq-XYZ] будет ошибочно изменен, и любые правила #foo будут ошибочно применены.

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

До
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
После
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Поскольку контейнер никогда не применит к себе первый атрибут ( cq-XYZ-A ), первый селектор будет соответствовать только в том случае, если другой родительский контейнер выполнил условия контейнера и применил его.

Относительные единицы контейнера

Контейнерные запросы также содержат несколько новых единиц измерения , которые вы можете использовать в CSS, например, cqw и cqh для 1% ширины и высоты (соответственно) ближайшего подходящего родительского контейнера. Для их поддержки единица измерения преобразуется в выражение calc(...) с помощью пользовательских свойств CSS . Полифил будет устанавливать значения для этих свойств с помощью встроенных стилей элемента контейнера.

До
.card {
  width: 10cqw;
  height: 10cqh;
}
После
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Существуют также логические единицы, такие как cqi и cqb , для линейного размера и размера блока (соответственно). Это немного сложнее, поскольку строчные и блочные оси определяются writing-mode элемента, использующего unit , а не запрашиваемого элемента. Чтобы поддержать это, полифил применяет встроенный стиль к любому элементу, writing-mode которого отличается от его родительского.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

Теперь единицы измерения можно преобразовать в соответствующее пользовательское свойство CSS, как и раньше.

Характеристики

Контейнерные запросы также добавляют несколько новых свойств CSS, таких как container-type и container-name . Поскольку такие API, как getComputedStyle(...) нельзя использовать с неизвестными или недопустимыми свойствами, после анализа они также преобразуются в пользовательские свойства CSS. Если свойство невозможно проанализировать (например, из-за того, что оно содержит недопустимое или неизвестное значение), его просто оставляют в покое для обработки браузером.

До
.card {
  container-name: card-container;
  container-type: inline-size;
}
После
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

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

До
@supports (container-type: inline-size) {
  /* ... */
}
После
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

По умолчанию пользовательские свойства CSS наследуются, что означает, например, что любой дочерний элемент .card будет принимать значения --cq-XYZ-container-name и --cq-XYZ-container-type . Родные свойства определенно ведут себя не так. Чтобы решить эту проблему, полифил вставит следующее правило перед любыми пользовательскими стилями, гарантируя, что каждый элемент получит начальные значения, если только оно не будет намеренно переопределено другим правилом.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

Лучшие практики

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

Во время начальной загрузки многое должно произойти, прежде чем полифилл сможет разметить страницу:

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

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

Чтобы вам было проще доставлять посетителям приятные впечатления, полифилл был разработан с учетом приоритета задержки первого ввода (FID) и накопительного смещения макета (CLS) , возможно, за счет наибольшей отрисовки контента (LCP) . Конкретно, полифилл не дает никаких гарантий, что ваши запросы к контейнеру будут оценены до первой отрисовки . Это означает, что для обеспечения наилучшего взаимодействия с пользователем вы должны убедиться, что любой контент, на размер или положение которого могут повлиять запросы контейнера, скрыт до тех пор, пока полифилл не загрузит и не перенесет ваш CSS. Один из способов добиться этого — использовать правило @supports :

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

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

Этот подход рекомендуется по ряду причин:

  • Чистый загрузчик CSS минимизирует накладные расходы для пользователей новых браузеров, обеспечивая при этом упрощенную обратную связь для пользователей старых браузеров и более медленных сетей.
  • Комбинируя абсолютное позиционирование загрузчика с visibility: hidden , вы избегаете смещения макета.
  • После загрузки полифила это условие @supports перестанет выполняться, и ваш контент будет раскрыт.
  • В браузерах со встроенной поддержкой контейнерных запросов условие никогда не будет выполнено, поэтому страница будет отображаться при первой отрисовке, как и ожидалось.

Заключение

Если вы заинтересованы в использовании контейнерных запросов в старых браузерах, попробуйте полифилл . Не стесняйтесь сообщать о проблеме, если у вас возникнут какие-либо проблемы.

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

Благодарности

Изображение героя Дэна Кристиана Падурца на Unsplash .