Как работает @supports

Перевод «How @supports Works» Криса Койера.

В CSS существует простой способ, позволяющий проверить браузерную поддержку какого-то свойства или комбинации свойства и значения перед тем, как применять определённый блок стилей. Похоже на то, как работает @media, проверяя ширину окна браузера перед тем, как применить стили. По тому же принципу CSS внутри будет применён, только если пара свойство: значение поддерживается в конкретном браузере. Эта директива называется @supports и выглядит так:

@supports (display: grid) {
.main {
display: grid;
}
}

Почему? Немного сложно объяснить. Лично я считаю, что не стоит постоянно её использовать. В CSS есть естественный механизм фолбэков. Если браузер не понимает какую-то пару свойство: значение, то он просто игнорирует её и применяет то, что было объявлено выше, спасибо каскаду. Часто этого вполне достаточно для фолбэка и в конечном счёте код получится не таким многословным. Я не из тех, кто считает, что всё должно выглядеть одинаково во всех браузерах. И я точно не из тех, кто пишет фолбэки для максимального сходства. Я предпочитаю писать код, в котором одно неподдерживаемое свойство не ломает всю функциональность.

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

Классическая ситуация

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

/* Здесь мы чуть позже напишем фолбэк */
@supports (display: grid) {
.photo-layout {
display: grid;
grid-template-columns: repeat(
auto-fill, minmax(200px, 1fr)
);
grid-gap: 2rem;
}
}

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

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

Я запустил старую версию мобильного Safari и много много много много много сайтов, на которых используются гриды, сломалось. Пожалуй, подожду ещё годик перед тем, как начать с ними возиться.
Дэвид Уэллс @DavidWells

Бывают случаи, когда допустимо не оставлять фолбэков, всё зависит от требований. Например, блоки выстроятся по вертикали вместо многостолбцовой сетки. Чаще всего это нормально. Но это недопустимо, например, для фотогалереи, для которой явно нужна базовая сетка на гридах. В нашем случае мы по умолчанию начнём с флексов и, используя @supports, применим гриды в тех браузерах, где они поддерживаются…

.photo-layout {
display: flex;
flex-wrap: wrap;
}
.photo-layout > div {
flex: 200px;
margin: 1rem;
}
@supports (display: grid) {
.photo-layout {
display: grid;
grid-template-columns: repeat(
auto-fill, minmax(200px, 1fr)
);
grid-gap: 2rem;
}
.photo-layout > div {
margin: 0;
}
}

Фолбэком в данном случае будет код за пределами блока @supports (свойства над тем блоком, который мы написали за один пример до этого). Блок @supports не добавляет специфичности. Поэтому мы должны соблюдать порядок следования свойств, чтобы убедиться, что переопределение сработает.

Заметили, что мне пришлось сбрасывать margin внутри блока @supports? Это то, что меня слегка раздражает. Между двумя описанными сценариями довольно много пересечений, поэтому вам обязательно нужно понимать, как они влияют друг на друга.

Желательно, чтобы эти два блока были совершенно логически независимы друг от друга…

Существует логическое НЕ для @supports, но это не значит, что его нужно постоянно использовать

Джен Симмонс разместила этот пример в статье Using Feature Queries in CSS пару лет назад:

/* ПЛОХОЙ ПРИЁМ если вы поддерживаете IE11, iOS 8 и старше */
@supports not (display: grid) {
/* Отдельный код для браузеров без поддержки гридов */
}
@supports (display: grid) {
/* Отдельный код для браузеров с поддержкой гридов */
}

Обратите внимание на оператор not в первом блоке. Он проверяет браузеры, которые не поддерживают гриды и применяет стили внутри блока только для них. Причина, по которой эта практика считается плохой, в том, что сама по себе поддержка @supports требует проверки! Вот что делает этот код коварным.

Писать код в логически разделённых блоках кажется очень привлекательной идеей. Потому что каждый блок будет срабатывать «с нуля» и не придётся переопределять предыдущие значения до умопомрачения. Но давайте вернёмся к ситуации с iOS, которую мы обсудили ранее… Поддержка @supports появилась только в 9 версии (в то время, когда флексбоксы доступны с 7 версии, а гриды — с 10.2). Это означает, что фолбэк на флексы, написанный внутри правила @supports с оператором not, не сработает в iOS 7 или 8. Это приводит нас к необходимости писать фолбэк для фолбэка для браузеров без поддержки @supports. Фух!

Большим достижением @supports является возможность учитывать совершенно разные реализации чего-либо. Гораздо проще становится размышлять о разных реализациях, когда блоки кода визуально отделены друг от друга.

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

@supports станет полезнее со временем

Как только @supports будет поддерживаться во всех нужных вам браузерах, это станет причиной для более агрессивного использования этой директивы без необходимости учитывать её поддержку. Так выглядит поддержка в браузерах:

Номера версий означают, что гриды поддерживаются с этой версии и выше

По сути, IE11 и любое устройство с iOS не старше 8 версии причиняют боль. Если в вашем проекте не требуется поддерживать эти браузеры, то вы можете свободно использовать @supports.

Ирония в том, что не существует множества возможностей CSS, которые могли бы понятно проиллюстрировать использование @supports. Но несколько всё же найдётся! Мы можем протестировать новые прикольные штуки при помощи Houdini:

Вы регулярно используете @supports? Какой основной сценарий использования? Могу я что-нибудь посмотреть?
Chris Coyier @chriscoyier
Использую на своём свадебном сайте для проверки поддержки Houdini 🎩🐰
Sam Richard @Snugug

(Я не совсем понимаю, что нужно обернуть в блок @supports в этом случае. Кто-нибудь ещё делал так?)

Когда @supports бесполезен

Я встречал ситуации, когда результат с использованием @supports точно такой же, как и без него. Например…

@supports (transform: rotate(5deg)) {
.avatar {
transform: rotate(5deg);
}
}

На определённом уровне в этом есть безупречный логический смысл. Если трансформации поддерживаются, то используем их. Но это не нужно, если в случае, когда поддержки нет, ничего не происходит. В данном случае трансформация и без @supports не сработает, если нет поддержки. И результат будет тот же.

Вот ещё один подобный пример.

Браузерные расширения для экспериментов с @supports

Существует два расширения!

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

Вот видео, в котором инструмент Кита применяется для сценария с использованием гридов и фолбэком на флексы:

Инструмент Ире, о котором она писала в статье Creating The Feature Queries Manager DevTools Extension, имеет кардинально другой принцип работы: он показывает те свойства, которые вы написали в CSS и создаёт переключатель для них. Таким образом вы можете включать и выключать отдельные свойства. Я не был уверен, что это сработает во фрейме. Поэтому я открыл Debug Mode и использовал этот инструмент в CodePen.

Больше примеров с @supports из реального мира

Вот один от Эрика Ворхеса. Он стилизовал кастомные чекбоксы и радио-кнопки, но обернул их в блок @supports. Ни один из описанных стилей не применится, если браузер не поддерживает одно из перечисленных проверяемых свойств.

@supports (transform: rotate(1turn)) and (opacity: 0) {
/* Все стили для реализации кастомных чекбоксов и радиокнопок */
}

Вот ещё несколько примеров, с которыми я столкнулся:

  • Джо Райт и Тиаго Нунес упоминали об использовании директивы для position: sticky. Я бы хотел увидеть демо! Если вы пишете position: sticky, то вам нужно предусмотреть сценарий на случай, когда свойство не поддерживается. А не просто позволить этому сломаться в браузерах без поддержки.
  • Кит Грант и Матиас Отт упомянули об использовании директивы для проверки object-fit: contain. У Матиаса есть демо, где трюк с позиционированием позволяет картинке заполнить всего родителя. Но в случае поддержки проверяемого свойства всё реализуется гораздо проще.
  • Райан Филлер упоминал об использовании директивы для проверки mix-blend-mode. В его примере у элемента задаётся прозрачность, но если mix-blend-mode поддерживается, то прозрачность уменьшается и таким образом достигается эффект наложения.
.thing {
opacity: 0.5;
}
@supports (mix-blend-mode: multiply) {
.thing {
mix-blend-mode: multiply;
opacity: 0.75;
}
}
  • Рик Шеннинк писал о свойстве backdrop-filter. Он говорит: «если оно поддерживается, то непрозрачность цвета фона требует тонкой настройки».
  • Нур Сауд упоминал, что директива может использоваться для определения Edge через поддержку свойства с определённым префиксом: @supports (-ms-ime-align:auto) { }.
  • Эмбер Вайнберг пишет о проверке clip-path. Размер и отступы картинки отличаются в ситуации, когда свойство недоступно.
  • Ральф Хольцманн говорит об использовании для проверки выреза на экране (переменная окружения).
  • Стейси Квернмо описывает случай проверки вариаций буквиц. Джен Симмонс описывает подобный сценарий в статье. В CSS существует прекрасное свойство initial-letter, но есть ситуации, когда вы не захотите применять его в сочетании с другими свойствами, если initial-letter не поддерживается (или если есть совершенно другой сценарий фолбэка).

Вот вам бонус от Ника Колли. В нём не используется @supports, но используется @media! Тот же принцип. Это поможет победить зависающий ховер на тачскринах:

@media (hover: hover) {
a:hover {
background: yellow;
}
}

Логика @supports

Основной сценарий:

@supports (initial-letter: 4) {
}

Оператор not:

@supports not (initial-letter: 4) {
}

Оператор and:

@supports (initial-letter: 4) and (transform: scale(2)) {
}

Оператор or:

@supports (initial-letter: 4) or (-webkit-initial-letter: 4) {
}

Комбо:

@supports ((display: -webkit-flex) or
(display: -moz-flex) or
(display: flex)) and (-webkit-appearance: caret) {
}

Вариант на JavaScript

В JavaScript есть API для проверки. Проверяем, существует ли свойство…

if (window.CSS && window.CSS.supports) {
// В старых версиях Opera была довольно
// странная реализация, поэтому пишем ещё это
// !!(
// (window.CSS && window.CSS.supports) ||
// window.supportsCSS || false
// )
}

Чтобы использовать его, передайте ему свойство в одном параметре, а значение в другом:

const supportsGrid = CSS.supports('display', 'grid');

…или передайте всё в одной строке с CSS-подобным синтаксисом:

const supportsGrid = CSS.supports('(display: grid)');

Проверка селекторов

В момент написания этой статьи только Firefox поддерживает подобный вариант проверки (включается через флаг), но вообще существует возможность тестировать селекторы при помощи @supports. Демо на MDN:

@supports selector(A > B) {
}