Cascade layers: наводим порядок в каскаде
Твой CSS разросся, новое правило не применяется, и ты дописываешь !important. Слои каскада убирают эту войну селекторов целиком.
Что ты научишься делать
- Разложишь стили по слоям так, чтобы порядок побеждал специфичность
- Перестанешь дописывать !important, чтобы перебить чужое правило
- Переведёшь существующий проект на слои, не тронув ни одного селектора
#Знакомая история
Проект живёт полгода. В стилях три тысячи строк. Ты добавляешь .button { background: teal; } — и кнопка остаётся серой.
Открываешь инструменты разработчика: правило зачёркнуто, победило .sidebar .panel .button из файла, который писал не ты. Ты дописываешь !important. Через месяц кто-то дописывает !important поверх твоего.
Это не проблема дисциплины. Это проблема архитектуры: у каскада нет способа сказать «сброс всегда слабее компонентов». Слои каскада (cascade layers) его дают.
#Что делает @layer
Слой — это именованная группа правил. Порядок объявления слоёв важнее любой специфичности внутри них.
Правило из слоя components побеждает правило из base всегда — даже если в base селектор из трёх идентификаторов, а в components один класс.
/* Один раз объявляем порядок — дальше он нерушим */@layer reset, base, components, utilities;@layer base { /* Даже такой тяжёлый селектор проиграет слою components */ #app .sidebar .panel .button { background: #999; }}@layer components { .button { background: teal; }}#Как это выглядит в реальном проекте
Рабочий набор слоёв почти всегда один и тот же. Порядок читается сверху вниз как «от самого слабого к самому сильному».
@layer reset, tokens, base, layout, components, utilities;@import url("reset.css") layer(reset);@import url("tokens.css") layer(tokens);@import url("typography.css") layer(base);@import url("grid.css") layer(layout);/* Чужая библиотека целиком уезжает в слабый слой */@import url("vendor/datepicker.css") layer(vendor);@layer components { .card { padding: 20px; border-radius: 14px; } .card__title { margin: 0 0 8px; font-size: 1.1rem; }}@layer utilities { /* Утилита обязана побеждать компонент — поэтому слой последний */ .is-hidden { display: none; } .text-center { text-align: center; }}#Вложенные слои
Слой можно разбить на подслои через точку. Порядок внутри работает по тем же правилам, а снаружи вся группа остаётся одним слоем.
Это нужно, когда компонентов становится много и внутри них появляется своя иерархия: сначала общие детали интерфейса, поверх — конкретные блоки страницы.
@layer components { @layer elements, blocks, pages;}@layer components.elements { .button { padding: 10px 18px; border-radius: 8px; }}@layer components.pages { /* Страничное правило перекроет общее, но останется слабее всего слоя utilities */ .checkout .button { width: 100%; }}Есть и анонимные слои — просто @layer { … } без имени. Такой слой нельзя дополнить позже и нельзя переупорядочить: он занимает место там, где написан.
Применение у них одно: изолировать кусок стороннего кода, к которому больше не планируешь возвращаться. Для своего кода имена почти всегда лучше.
#Как перевести существующий проект
Переезд не требует переписывать селекторы. Он делается за один вечер и откатывается одной строкой.
Объяви порядок первой строкой
В главный файл, до всех импортов:
@layer reset, vendor, base, layout, components, utilities;. Пока ничего не изменилось — слои объявлены, но пусты.Загони чужое в vendor
Все библиотеки подключи как
@import url("lib.css") layer(vendor). Уже на этом шаге исчезнет половина твоих!important.Перенеси сброс
Оберни
reset.cssилиnormalize.cssв слойreset. Теперь сброс перестанет спорить с компонентами.Раздели своё
Типографика и теговые селекторы — в
base. Сетки и контейнеры — вlayout. Всё, у чего есть класс-компонент, — вcomponents.Утилиты — последними
Классы вроде
.is-hiddenи.text-centerположи вutilities. Они должны побеждать компоненты, и теперь побеждают без!important.Убери важные объявления
Пройдись по
!importantи удаляй по одному, проверяя страницу. Большая часть окажется лишней — её работу теперь делает порядок слоёв.
#Когда НЕ надо
Слои — инструмент против большого чужого кода. У маленького проекта их цена выше пользы.
- Лендинг на 200 строк. Порядок правил и так виден целиком. Слои добавят уровень вложенности и ничего не решат.
- Как замена дисциплине. Если внутри слоя
componentsпродолжают писать селекторы из пяти частей, война просто переедет внутрь слоя. - Ради «модно». Слои усложняют отладку: в инструментах разработчика придётся смотреть не только на специфичность, но и на слой. Заводить их стоит, когда боль уже есть.
#Что слои не чинят
Слои управляют только приоритетом правил. Всё остальное остаётся на тебе.
Они не уменьшают количество кода: три тысячи строк останутся тремя тысячами, просто перестанут спорить друг с другом. Они не наводят порядок в именах: .block-2-final в слое components так и останется непонятным. И они не отменяют наследование — унаследованный color приходит от родителя мимо всех слоёв.
Ещё одна тонкость: слои никак не влияют на порядок загрузки. Если файл подключён позже, его правила всё равно применятся позже — просто внутри своего слоя.
Так не надо
.sidebar .card .title {
font-size: 18px !important;
}Классический ответ на «стиль не применяется»: утяжелить селектор и добавить важность. Работает ровно до следующего такого же правила.
Так надо
@layer components {
.card__title { font-size: 18px; }
}Один класс, никакой важности. Правило победит базовый слой по определению, а не по весу селектора.
#Поддержка браузерами
| Браузер | Состояние |
|---|---|
| Chrome, Edge | Поддерживают с 2022 года |
| Firefox | Поддерживает с 2022 года |
| Safari | Поддерживает с 2022 года, включая мобильный |
| Старые версии | Правила внутри @layer просто игнорируются целиком |
Где спотыкаются почти все
Порядок слоёв объявлен не первой строкой
Почему Порядок задаётся тем, где слой впервые упомянут. Если @layer utilities встретился раньше components, утилиты станут слабее — и никто не поймёт почему.
Как надо Первой строкой файла объявляй порядок целиком: @layer reset, base, components, utilities;
Половина стилей осталась вне слоёв
Почему Внеслойные правила сильнее любого слоя. Получается та же война, только теперь ещё и неочевидная.
Как надо Либо всё в слоях, либо вне слоёв держи один осознанный набор — например, стили для печати.
!important внутри слоя ведёт себя «наоборот»
Почему Для важных объявлений порядок слоёв переворачивается: !important в самом слабом слое побеждает !important в сильном. Это логично (слабый слой не должен терять свои крайние меры), но неожиданно.
Как надо Не смешивай !important со слоями. Если завёл слои — важные объявления почти всегда лишние.
- Слой каскада cascade layer
- Именованная группа правил. Порядок слоёв важнее специфичности селекторов внутри них.
- Внеслойные стили unlayered styles
- Правила вне любого
@layer. Считаются самым сильным слоем. - Слой vendor vendor layer
- Приём: чужую библиотеку подключают в отдельный слабый слой, чтобы она не перебивала свои стили.
Проверь себя
В этом и смысл слоёв: порядок объявления сильнее специфичности. Внутри одного слоя специфичность снова начинает работать как обычно.
Внеслойные стили считаются последним слоем. Сделано ради совместимости со старым кодом, но об этом легко забыть.
Библиотека уезжает в слабый слой целиком — и любой твой компонентный класс её перекрывает.
Непонятное @-правило игнорируется целиком, вместе с содержимым. Со слоями это опаснее, чем с обычными свойствами.
Задание
Разложить свалку по слоям
В CSS справа четыре группы правил, и они дерутся: сброс перебивает компоненты, утилита не работает. Расставь слои так, чтобы кнопка стала бирюзовой, а класс .is-hidden действительно прятал элемент.
Условие: не меняй ни одного селектора — только оберни правила в @layer и объяви порядок.
Подсказка 1
Первой строкой: @layer reset, base, components, utilities; — дальше просто разложи существующие правила по этим слоям.
Подсказка 2
Правило для button — это сброс, два тяжёлых селектора — база, .button с отступами — компонент, .is-hidden — утилита.
Показать решение
<button class="button">Кнопка</button><p class="note is-hidden">Этот абзац должен быть скрыт</p>@layer reset, base, components, utilities;@layer reset { button { background: #999; color: #fff; }}@layer base { #app .button, .panel .button { background: #777; }}@layer components { .button { padding: 10px 18px; background: teal; border: 0; border-radius: 8px; font: inherit; color: #fff; }}@layer utilities { .is-hidden { display: none; }}