С в CSS означает «Каскад».

Недавно CSS исполнилось 25 лет. Все это время мы ищем способы управлять каскадом с минимальными потерями и сложностями. И скоро у нас появится новый — очень мощный — способ делать это. В 2021 была разработана новая спецификация — каскадные слои (Cascade Layers). Сейчас она уже является официальным кандидатом в рекомендации, так что пора посмотреть, что это за зверь.

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

Мы придумали множество способов контролировать это безумие: от откровенно плохих (модификатор !important) до вполне приемлемых (методологии ISCSI и BEM). Не так давно появились нативные методы управления каскадом: псевдоклассы :where/:is. Каскадные слои станут следующим шагом, который даст нам новые возможности.

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

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

  • Chrome Canary/Chromium 99+,
  • Firefox Nightly 97+,
  • Safari Technical Preview 137+.

Место каскадных слоев в каскаде стилей

«каскадные слои позволят разработчикам определять собственную схему наслоения стилей и избегать конфликтов специфичности и порядка источников»

оригинальное объяснение Miriam Suzanne, автора спецификации каскадных слоев (некоторая информация устарела)

Чтобы понять, в чем ценность слоев, нам нужно чуть лучше разобраться в самом каскаде и понять, как именно браузер определяет, какие стили применить к элементу.

Источники стилей

Все начинается с источника стилей (origin).

У браузера есть три основных источника:

  • Авторские стили (Author origin). Те, которые вы сами написали для своего сайта в CSS-файлах, включая стили различных библиотек.
  • Пользовательские стили (User origin). Предпочтения пользователя, сохраненные в настройках браузера, например, размер шрифта.
  • Стили браузера (User-Agent origin). Дефолтное оформление, которое браузер применяет к HTML-страницам.

Кроме того, источниками считаются transitions (переходы) и animations (анимации). Во время выполнения они создают «виртуальные» правила стилей, которые имеют самый высокий приоритет. Но сейчас мы не будем их учитывать.

Каждое правило (из любого источника) может быть отмечено флагом !important, что увеличивает его приоритет. При этом important-правила с менее приоритетного источника важнее, чем important-правила с более приоритетного источника (порядок источников меняется).

Вот так выглядит итоговая шкала приоритетов:

  1. стили браузера + !important,
  2. стили пользователя + !important,
  3. авторские стили + !important,
  4. обычные авторские стили,
  5. обычные стили пользователя,
  6. обычные стили браузера.

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

Независимо от того, как вы пишете стили — с помощью препроцессоров или без них, используя BEM или CSS-in-JS — вы всегда работаете внутри «авторского» источника стилей.

Специфичность и порядок появления

При сортировке стилей (определении приоритета) учитывается несколько параметров (включая источник). Вот их полный список в порядке убывания значения:

  1. Источник и важность (модификатор !important).
  2. Область видимости. Например, теневой DOM создает собственный контекст стилизации, поэтому стили внутри него не зависят от стилей самой страницы.
  3. Инлайн-стили, привязанные к конкретному элементу с помощью атрибута style.
  4. Специфичность селекторов.
  5. Порядок появления стилей в коде — работает по правилу «последний побеждает».

В рамках «авторского» источника стилей мы можем управлять двумя последними параметрами: специфичностью правил и порядком появления стилей. И это совсем не просто.

Рассмотрим маленький пример. У нас есть абзац с классом .royal:

<p class="royal">Lorem, ipsum dolor.</p>

И вот такая таблица стилей:

p {
  color: green;
}

.royal {
  color: royalblue;
}

:first-child {
  color: red;
}

Каким в итоге будет цвет текста абзаца?

Селектор по тегу имеет самую низкую специфичность, не учитываем его. Однако специфичность селектора по классу (.royal) и псевдокласса (:first-child) одинакова. В этом случае браузер смотрит на порядок появления правил в коде — и :first-child побеждает, так как идет после .royal. В итоге, текст абзаца будет красным.

Если же вы хотите, чтобы применились стили класса royal, вы можете сделать следующее:

  • добавить к определению color модификатор !important;
  • переместить правило .royal после :first-child;
  • увеличить специфичность селектор, превратив его, например, в p.royal.

Но каждое из этих решений имеет последствия:

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

Чтобы не потерять контроль над стилями, мы используем различные «ненативные» способы управления каскадом. Например, методология BEM или CSS-in-JS-решения управляют специфичностью селекторов, поддерживая ее на уровне селектора по классу.

Каскадные слои после своего появления встанут между 3 и 4 пунктами (между инлайн-стилями и специфичностью селекторов). Другими словами, они дадут нам способ явно указать приоритет стилей, без оглядки на их специфичность и порядок появления.

Создание и порядок слоев

Для создания каскадных слоев предназначено новое @-правило @layer.

Самый простой способ создать слой — прямое объявление:

@layer base {
  body { ... }
}

Мы создали слой с именем base и сразу определили для него какие-то стили.

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

@import url(bootstrap.css) layer(bootstrap);

Кроме того, правило @layer может использоваться для указания порядка слоев. Нужно просто перечислить имена слоев в нужной последовательности.

@layer bootstrap, base, application;

Эта строчка создаст три слоя, а стили на эти слои можно добавить позже в коде.

@layer bootstrap, base, application;

@import url(bootstrap.css) layer(bootstrap);

@layer base {
  body {... }
}

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

В примере стили фреймворка bootstrap находятся на самом нижнем уровне (слой bootstrap), поверх них находятся базовые стили (слой base). И наконец на самом верху, с максимальным приоритетом расположен слой application (основные стили приложения).

Обратите внимание, в спецификации есть указания об использовании правила @import. Нельзя вызывать @import после @layer, поэтому если вам нужно сделать несколько импортов, сгруппируйте их вместе.

Вы можете добавлять стили в уже созданные ранее слои, обращаясь к ним по имени:

@layer base {
  a { }
}

@layer base {
  p { } 
}

Второй блок @layer просто добавит новые стили к уже созданному слою base, не перезаписывая его.

Вложенные слои

Слои можно вкладывать друг в друга. Сослаться на вложенный слой можно через родительский, используя оператор . (точка), как в JavaScript.

@layer framework {
  @layer reset, base, components;
}

@layer framework.reset { ... }

Порядок вложенных слоев также можно определить заранее, перечислив несколько в одной инструкции @layer:

@layer framework.reset, framework.base, framework.components;

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

@layer base;

@layer framework {
  @layer base { ... }
}

В этом примере создается новый слой framework.base, отличный от слоя base.

Анонимные слои

На самом деле давать имя слою необязательно, вполне можно использовать анонимные слои.

@layer { /* rules */ }
@import url(framework.css) layer;

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

Анонимные слои можно использовать в качестве “приватных” слоев, если вы намеренно не хотите, чтобы кто-то другой мог ими манипулировать.

Специфичность каскадных слоев

Каким же образом каскадные слои влияют на приоритет стилей?

Внутри слоев продолжают действовать все рассмотренные ранее правила (специфичность и порядок появления селекторов). Однако на уровне самих слоев они перестают иметь значение.

Более «поздний» слой перекрывает более «ранний» независимо от специфичности селекторов на нем.

@layer reset, base, theme, components, utilities;

Любые селекторы, определенные в слое theme, будут иметь приоритет над любыми селекторами из слоев reset или base. А стили из слоя utilities будут самыми приоритетными, даже если это простые селекторы по тегу.

Специфичность слоя важнее, чем специфичность селектора в нем.

@layer base {
  article h2 {
    color: purple;
  }
}

@layer theme {
  h2 {
    color: blue;
  }
}

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

Помним, что порядок слоев можно определить явно, с помощью @layer.

Для вложенных слоев все работает точно так же. А в пределах одного слоя по-прежнему применяются классические правила определения специфичности.

Теперь должно быть более понятно, почему слои называются «слоями». Их можно сравнить со слоями в Photoshop, где верхний слой перекрывает нижние, но в тех местах, где верхний слой прозрачен (не устанавливает каких-то правил), нижние все равно «просвечивают».

Специфичность стилей вне слоев

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

Было решено, что стили «вне слоев», то есть обычные стили, которые мы используем прямо сейчас, всегда будут иметь наивысший приоритет. Их специфичность выше, чем специфичность слоев.

@layer utilities {
  .blue {
    color: blue;
  }
}

/* побеждает */
p {
  color: red;
}

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

Стили внутри слоя и вложенные слои

Спустимся на уровень ниже. Представим, что у нас есть слой, внутри которого есть какие-то отдельные правила стилей, а также вложенные слои.

@layer typography {
  // стили "без слоя"
  p {
    color: green;
  }

  // вложенный слой
  @layer content;
}

@layer typography.content {
  p {
    color: blue;
  }
}

Здесь все работает точно так же. Обычные стили («вне слоя») приоритетнее, чем слои, которые находятся на том же уровне. Цвет абзаца останется зеленым.

Использование !important в слоях

Как и в случае с обычной каскадной сортировкой стилей без слоев, модификатор !important повышает приоритет свойства, объявленного внутри слоя. «Важные» стили всегда приоритетнее, чем «неважные» на том же или другом слое.

Обратите внимание: если несколько «важных» стилей конкурируют, то победит тот, который был определен РАНЬШЕ. Такое поведение соответствует механизму работы модификатора !important для разных источников стилей, который мы рассматривали в начале статьи.

Кроме того, «важные» стили в слое приоритетнее, чем «неважные» стили вне слоев.


// побеждает
@layer theme {
  .lead {
    color: green !important;
  }
}

@layer utilities {
  .lead {
    color: red !important;
  }
}

.lead {
  color: orange !important;
}

В этом фрагменте кода у нас два каскадных слоя и одно правило «вне слоев». Все стили имеют модификатор !important. Победит «важное» правило с самого первого слоя.

На картинке обозначен приоритет применения стилей:

Все очень непросто, поэтому лучше вообще не использовать в ваших стилях модификатор !important.

Вот небольшой пример, иллюстрирующий все, что мы только что рассмотрели:

See the Pen @layer specificity and !important by Smashing Magazine (@smashingmag) on CodePen.

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

Использование каскадных слоев вместе с препроцессорами

Препроцессоры CSS (Sass, Less) позволяют комбинировать стили из нескольких таблиц стилей.

Например, в Sass можно подключать другие файлы с помощью директивы @use:

@use "reset";
@use "theme";

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

@use 'sass:meta';

@layer theme {
  @include meta.load-css('theme');
}

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

@use "sass:meta";
@use "sass:list";

$layers: "reset", "theme" !default;

// список слоев
@layer #{$layers};

// вывод каждого слоя с соответствующими стилями
@each $layer in $layers {
  @layer #{$layer} {
    @include meta.load-css($layer);
  }
}

Примеры использования каскадных слоев

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

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

Сброс стилей или базовые стили

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

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

Переопределение стилей фреймворков и библиотек

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

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

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

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

Темы оформления

Каскадные слои можно создавать (и добавлять на них стили) в рамках других @-правил, например, @media. Обновление стилей слоя внутри медиа-запроса сохраняет первоначальную специфичность слоев. Это делает ваши стили более гибкими.

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

@layer theme {
  @layer light, dark, prefers-contrast, forced-colors, user;

  @layer light {
    body {
      --background: #f9f9f9;
      --color: #222;

      background-color: var(--background);
      color: var(--color);
    }
  }
}

@media (prefers-color-scheme: dark) {
  @layer theme.dark {
    body {
      --background: #222;
      --color: #fff;
    }
  }
}

Здесь мы определили несколько возможных тем, связанных с медиа-запросами, а также последний слой user для пользовательской темы. По дефолту используется светлая тема, но в медиа-запросе prefers-color-scheme мы устанавливаем стили для темной темы, которые имеют более высокий приоритет (их слой лежит выше).

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

Компоненты

Возможно, вы сразу подумали о создании отдельного каскадного слоя для каждого UI-компонента. Это не самое лучшее решение.

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

Каскадные слои не предназначены для определения области видимости или инкапсуляции стилей. Для этого у нас скоро будет другая спецификация нативных областей видимости CSS от Miriam Suzanne.

Состояния элементов

Прекрасный пример использования каскадных слоев — выделение стилей для разных состояний элементов, таких как :disabled или :focus.

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

.button:not(:disabled) { ... }

Это правило увеличивает специфичность и переопределить его в будущем довольно сложно. Слои же позволят делать то же самое намного проще.

@layer components, states;

@layer components {
  .button {
    --focus-color: rebeccapurple;
    
    background-color: rebeccapurple;
    color: #fff;
  }
}

@layer states {
  :disabled {
    background-color: #ddd;
    color: #999;
  }
  
  :focus-visible {
    outline: 2px solid var(--focus-color, currentColor);
    outline-offset: 2px;
  }
}

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

Нужны ли нам каскадные слои?

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

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

Особенно полезны слои будут для разработчиков фреймворков и дизайн-систем.

Можно ли использовать каскадные слои прямо сейчас?

Многие свежие фичи CSS мы можем начать использовать почти сразу в качестве прогрессивного улучшения, не дожидаясь полной поддержки в браузерах. Например, для свойств aspect-ratio или селекторов вроде :is() мы можем использовать директиву @supports, чтобы проверить, поддерживаются ли они. Или же, наоборот, можно использовать метод graceful degradation, добавляя адекватный фоллбэк на случай, если фича не поддерживается.

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

Альтернативные решения для управления специфичностью

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

Псевдоклассы :is() и :where() уже хорошо поддерживаются evergreen-браузерами. В скобках им можно передавать несколько селекторов. Разница заключается в том, как они работают со специфичностью.

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

Например, мне нравится удалять list-style, если элемент списка имеет атрибут role="list" (чтобы вспомогательные технологии по-прежнему считали его списком). Но добавление селектора по атрибуту повышает специфичность, поэтому для переопределения стилей придется снова добавлять его. Вместо этого можно использовать :where() для создания селектора с нулевой специфичностью, который очень легко переопределить.

:where(ul, ol):where([role="list"]) { ... }

Статус спецификации

Каскадные слои — это часть спецификации CSS Cascading and Inheritance Level 5, которая стала кандидатом в рекомендации 13 января 2022 года. Все решенные ишьюс вы можете найти на GitHub (и добавить собственные, если найдете).

Полезные ссылки

0 комментариев

Оставить комментарий

*Доступные HTML-теги: a, abbr, blockquote, code, pre, del, i, em, strong, b, strike
*Не будет опубликован