Вольный перевод статьи Hello, CSS Cascade Layers от Ahmad Shadeed
Одной из наиболее распространенных путаниц в CSS является специфичность (specificity) при написании стилей. Например, меняя значение display
для элемента никогда не сработает, если другой элемент в каскаде переопределяет его по причине более высокой специфичности. Или когда какой-то элемент имеет !important
. Обычно это происходит, когда кодовая база растет и мы не структурируем CSS по пути избежания (или сокращения) таких проблем.
Чтобы преодолеть проблемы с каскадом и специфичностью, нам нужно быть осторожными в местах, где мы пишем CSS. В малых проектах всё может быть в порядке, однако в больших проектах задача может оказаться трудоёмкой. В результате стали появляться различные методы для организации CSS и, соответственно, сокращения проблем с каскадом. Первые три, что приходят мне на ум, это BEM, Smacss от Jonathan Snook и Inverted Triangle CSS от Harry Roberts
В этой статье мы рассмотрим, как работают каскадные слои и как они помогут нам писать CSS с большей уверенностью, а также примеры их использования.
Проблема
Основная проблема, которую решают каскадные слои, это гарантированный способ написания CSS, не беспокоясь о специфичности и порядке исходного кода. Давайте обратимся к примеру, чтобы проиллюстрировать проблему.
У нас есть кнопка с двумя стилями: default и ghost. В HTML мы будем их использовать следующим образом:
<footer class="form-actions">
<button class="button">Save edits</button>
<button class="button button--ghost">Cancel</button>
</footer>
Всё отлично работает в этом случае. Но что, если у нас появился третий вариант кнопки и мы не можем написать его сразу после селектора .button
?
.button
идёт после .button--facebook
. В итоге он будет переопределён. В данном случае можно найти решение через повышение специфичности для .button-facebook
, например:
.some-parent .button--facebook {
background-color: var(--brand-fb);
color: #fff;
}
Или можно сделать так (не повторяйте это в домашних условиях!):
.button--facebook {
background-color: var(--brand-fb) !important;
color: #fff !important;
}
Оба решения не так уж хороши. Намного лучше расположить их в корректном месте, прямо после .button
. Непростая работа без помощи CSS препроцессора (например, Sass), который поможет разделить файлы CSS на части и компоненты.
Введение в CSS Cascade Layers (каскадные слои)
Каскадные слои это новая CSS фича, которая позволяет разработчикам получать больший контроль над написанием CSS в больших проектах. По словам автора спецификации Miriam Suzanne:
Каскадные слои позволят авторам управлять своей внутренней каскадной логикой, не полагаясь полностью на эвристику специфичности или исходный порядок.
Давайте применим каскадные слои для предыдущего примера.
Для начала надо обозначит слой. Чтобы это сделать, мы напишем @layer
.
@layer components { }
Я обозначил слой с именем components
. Внутри этого слоя мне нужно определить default стили кнопки.
@layer components {
.button {
color: #fff;
background-color: #d73a7c;
}
}
Шик. Далее нужно добавить слой для вариаций.
@layer components {
.button {
color: #fff;
background-color: #d73a7c;
}
}
@layer variations {
.button--ghost {
background-color: transparent;
color: #474747;
border: 2px solid #e0e0e0;
}
}
Вот визуализация слоев. Они аналогичны слоям Photoshop, поскольку то, что определено последним в CSS, будет первым в списке слоев визуально.
В нашем примере variation
слой определен последним, поэтому он будет иметь более высокий приоритет, чем слой components
.
Существует также другой способ организации того, какой слой переопределяет другой, путем одновременного определения слоев.
@layer components, variations;
Вернёмся к нашему примеру. Основная проблема была в том, что нам нужно было создать новую вариацию кнопки, поместив её в место, где вариация бы имела меньшую специфичность (до .button
). С каскадными слоями мы можем добавить этот кусок в слой variations
.
@layer components, variations;
@layer components {
.button {
color: #fff;
background-color: #d73a7c;
}
}
@layer variations {
.button--ghost {
background-color: transparent;
color: #474747;
border: 2px solid #e0e0e0;
}
.button--facebook {
background-color: var(--brand-fb);
}
}
Таким образом, мы всегда можем гарантировать, что вариация всегда будет иметь приоритет над default стилями. Давайте рассмотрим объяснение выше визуально.
Обратите внимание, как каждая кнопка живет в слое. Порядок соответствует определению @layer
вверху.
Если поменять порядок, то слой components
будет переопределять слой variations
и default стили "победят".
Добавление стилей в слои
В каскадных слоях браузер комбинирует стили из одних и тех же селекторов @layer
и считывает их сразу в соответствии с их порядком.
Рассмотрим следующее:
@layer components, variations;
@layer components {
.button {..}
}
@layer variations {
.button--ghost {..}
}
/* 500 lines later */
@layer variations {
.button--facebook {..}
}
Браузер поместит .button--facebook
прямо после .button--ghost
в слое variations
. Изображение для большей ясности:
Поддержка браузеров
Это самый важный вопрос, чтобы задуматься над новой фичей CSS. Согласно Can I Use на момент написания этой статьи, она поддерживается в Firefox, Chrome, Safari TP. Можем ли мы использовать их в качестве enhancement? Нет, мы не можем. Если только мы не используем полифилл Javascript (которого пока нет).
Где в каскаде живут слои?
Чтобы ответить на этот вопрос, давайте рассмотрим каскад в CSS.
CSS каскад упорядочен следующим образом (первые имеют больший приоритет):
- Origin и importance
- Инлайн стилизация
- Слои
- Специфичность
- Порядок в коде Рассмотрим следующий рисунок. Чем толще линия, тем приоритетнее стиль в каскаде.
Origin и importance
Это две разные (но связанные) вещи, так что разберем каждую по-отдельности.
Origin стили приходят из следующих вещей (сначала более приоритетные):
- Стили разработчика (AKA авторские)
- Стили пользователя
- Стили браузера
Это означает, что CSS, написанный разработчиком, всегда будет побеждать стили пользователя и браузера.
Посмотрим пример.
html {
font-size: 16px;
}
Если пользователь попробует поменять стили браузера (font-size по-умолчанию в данном примере), то CSS выше всё равно его переопределит, так как стили разработчика приоритетнее пользовательских и браузерных.
Это плохая практика для accessibility (доступности). Пожалуйста, не делайте этого в продакшене. Я просто добавил это ради объяснения origin стилей.
Что касается стилей браузера, они предназначены для user agent стилей. Например, по-умолчанию стиль <button>
выглядит по-разному в браузерах. Мы можем его переопределить, как вы уже догадались, потому что стили разработчика имеют преимущество над стилями браузера.
Если вы посмотрите на дефолтные стили кнопки, то увидите user agent stylesheet, которые как раз и показывают стили браузера у кнопки.
Всё вышеописанное касалось стандартных/нормальных/обычных правил. Это значит, что они не имеют ключевого слова !important
. В случае, если оно есть, то порядок приоритета будет следующим:
- Стили браузера с important
- Стили пользователя с important
- Стили разработчика с important
- Обычные стили разработчика
- Обычные стили пользователя
- Обычные стили браузера
Инлайн (inline) стили
Если элемент имеет инлайн стили, то у него большая специфичность по сравнению с другими правилами того же importance уровня.
В примере ниже, цвет кнопки будет #fff
так как инлайн стили в приоритете.
<style>
button {
color: #222;
}
</style>
<button style="color: #fff;">Send</button>
От себя: Importance обеих стилей относятся к "Обычные стили разработчика", следовательно далее по уровню проверяется inline. Если бы был у них разный уровень importance, например color: #222; !important
, то это правило отнеслось бы к категории "Стили разработчика с important", что приоритетнее, чем "Обычные стили разработчика". Тогда был бы цвет #222
.
Слои
О, привет слои! Это новый гость в каскаде. Каскадные слои имеют больший приоритет, чем специфичность селектора. В следующем примере сможете ли вы угадать размер шрифта элемента p
в слое custom
?
@layer base, custom;
@layer base {
#page .prose p {
font-size: 1rem;
}
}
@layer custom {
p {
font-size: 2rem;
}
}
Шрифт будет 2rem
. В каскадных слоях не важна специфичность. Она будет проигнорирована, если элемент переопределен последующим слоем.
Специфичность
После слоёв браузер смотри на правила CSS и определяет, какой из них побеждает по сравнению с другими, отталкиваясь от специфичности.
Вот простой пример. .button
внутри .newsletter
имеет большую специфичность чем просто .button
. В итоге верхнее правило будет переопределено.
.button {
padding: 1rem 1.5rem;
}
/* Побеждает */
.newsletter .button {
padding: 0.5rem 1rem;
}
Порядок в коде
Наконец порядок вступает в силу. Когда два элемента имеют одинаковую специфичность, тогда их порядок в документе определяет победителя (тот, что ниже "сильнее").
.newsletter .button {
padding: 1rem 1.5rem;
}
/* Побеждает */
.newsletter .button {
padding: 0.5rem 1rem;
}
Теперь у вас есть понимание, где слои находятся в каскаде. Обратимся к нескольким примерам их использования.
Применение каскадных слоёв
Я попробовал посмотреть в текущих проектах, где бы каскадных слои могли пригодиться и придумал несколько случаев.
Переключение UI темы
В проекте, над которым я работаю, использование каскадных слоев для темизации UI будет идеальным решением. Проблема, которую он решает здесь, состоит в том, чтобы позволить мне, как разработчику, переключаться между темами без изменения CSS или изменения их порядка тем или иным образом.
@layer base, elements, objects, components, pages, themes;
У меня есть несколько слоёв и последний это themes
. Слой themes
может в себе иметь несколько слоев (да, каскадные слои поддерживают вложенность).
Обратите внимание, что вверху я определил @layer custom, default
. default
будет переопределять custom
.
@layer base, elements, objects, components, pages, themes;
@layer themes {
@layer custom, default;
@layer default {
:root {
--color-primary: #1877f2;
}
}
@layer custom {
:root {
--color-primary: #d73a7c;
}
}
}
Если вам потребуется переключить темы, нужно просто поменять порядок слоев в первой строчке @layer themes
.
@layer base, elements, objects, components, pages, themes;
@layer themes {
/* Custom is active */
@layer default, custom;
@layer default {
:root {
--color-primary: #1877f2;
}
}
@layer custom {
:root {
--color-primary: #d73a7c;
}
}
}
Сторонний CSS
Я взял пример, в котором используется карусель flickity. Посмотрите на все ! important
значения.
.flickity-page-dots {
bottom: 20px !important;
}
.flickity-page-dots .dot {
background: #fff !important;
opacity: 0.35 !important;
}
.flickity-page-dots .dot.is-selected {
opacity: 1 !important;
}
С каскадными слоями мы можем добавить сторонний CSS перед слоем компонентов. Мы можем импортировать внешний файл CSS и задать ему слой.
@layer base, vendors, components;
@layer base {
/* Base styles */
}
/* Import a .css file and assign it to a layer */
@import url(flickity.css) layer(vendors);
@layer components {
.flickity-page-dots {
bottom: 20px;
}
.dot {
background: #fff;
opacity: 0.35;
}
.dot.is-selected {
opacity: 1;
}
}
Меньше переживать об ошибках специфичности
Допустим, у нас есть компонент списка, и нам нужен вариант, в котором список имеет меньший margin.
<ul class="list">
<li class="list__item list__item--compact">Item 1</li>
<!-- Other items -->
</ul>
Поскольку псевдоселектор :not
придает элементу большую специфичность, его нельзя переопределить без повторного использования :not
. Рассмотрим следующее:
/* Побеждает */
.list__item:not(:last-child) {
margin-bottom: 2rem;
outline: solid 1px #222;
}
.list__item--compact {
margin-bottom: 1rem;
}
.list__item--compact
не будет переопределять .list__item
^ так как последний имеет большую специфичность из-за использования :not
. Чтобы все заработало, нам нужно написать следующее:
.list__item:not(:last-child) {
margin-bottom: 2rem;
outline: solid 1px #222;
}
.list__item--compact:not(:last-child) {
margin-bottom: 1rem;
}
Посмотрим, как с этой проблемой справятся каскадные слои.
Представим, что @layer list
содержит base
и overrides
слои. В overrides
я написал альтернативный способ, и он работал, как и ожидалось, поскольку overrides
является последним слоем.
@layer list {
@layer base, overrides;
@layer base {
.list__item:not(:last-child) {
margin-bottom: 2rem;
}
}
@layer overrides {
.list__item--compact {
margin-bottom: 1rem;
}
}
}
Вложенные компоненты
В данном примере у нас есть список действий (лайк, комментарий) для основного элемента социальной ленты и еще один список для каждого комментария.
Иконка у блока элемента ленты имеет размер 24px
. В компоненте комментариев размер меньше.
@layer feed, comments;
@layer feed {
.feed-item .c-icon {
width: 24px;
height: 24px;
}
}
@layer comments {
.comment__icon {
width: 18px;
height: 18px;
}
}
Обратите внимание, что .feed-item .c-icon
имеет большую специфичность, чем .comment__icon
, но в этом фишка использования каскадных слоев!
Служебный (утилитный) CSS
Мы привыкли добавлять !important
к служебным классам CSS, чтобы они всегда применялись к элементу. С каскадными слоями мы можем разместить слов на последнем месте.
Рассмотрим следующий пример. У нас есть header страницы с утилитным классом p-0
. Мы хотим сбросить padding до 0.
<div class="c-page-header p-0">
<!-- Content -->
</div>
Вот как это выглядит с каскадными слоями.
@layer base, vendors, components, utils;
@layer components {
@layer page-header {
.c-page-header {
padding: 1rem 2rem;
}
}
}
@layer utils {
.p-0 {
padding-left: 0;
padding-right: 0;
}
}
Больше подробностей о каскадных слоях
Стили без слоя имеют большую специфичность
Если есть стили CSS, которые не назначены слою, то они будут добавлены к неявному последнему слою.
Рассмотрим следующий пример.
.button {
border: 2px solid lightgrey;
}
@layer base, components;
@layer base {/* Base styles */}
@layer components {
.button {
border: 0;
}
}
В этом примере правило .button
определено без @layer
, однако браузер поместит его в неявный слой.
@layer base, components;
@layer base {/* Base styles */}
@layer components {
.button {
border: 0;
}
}
/* Implicit layer */
@layer {
.button {
border: 2px solid lightgrey;
}
}
Вывод
Каскадные слои — обалденная CSS фича, и, как вы видели в примерах, она может быть весьма полезной. Единственным ограничением для меня является то, что мы не сможем использовать его как улучшение (enhancement) только с помощью CSS. Это может немного замедлить внедрение слоев в веб-сообществе.
Для дальнейшего изучения
-The Future of CSS: Cascade Layers by Bramus Van Damme
-Getting Started With CSS Cascade Layers by Stephanie Eckles
-Cascade layers are coming to your browser by Una Kravets
Top comments (2)
Желание нарастить специфичность - это самое дурное желание верстальщика, так появляется facepalm-code. Самый первый пример с
.button--facebook
решается через контекст css-переменных: обычной кнопке свойства указываешь через переменную, тогда селектор контекста.button--facebook{ --button-color: blue }
можно размещать как до, так и после селектора.button { color: var(--button-color) }
А при изменении порядка слоев, они меняются для всего проекта глобально? Или локально, для скоупа, в котором находятся, как css-переменные?