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


Это полное руководство по каскадным слоям — функционалу CSS, позволяющему определять явные слои специфичности и полностью контролировать приоритет стилей в проекте, не применяя «костыли» специфичности или !important. 

Небольшой пример

/* establish a layer order up-front, from lowest to highest priority */
@layer reset, defaults, patterns, components, utilities, overrides;

/* import stylesheets into a layer (dot syntax represents nesting) */
@import url('framework.css') layer(components.framework);

/* add styles to layers */
@layer utilities {
  /* high layer priority, despite low specificity */
  [data-color='brand'] { 
    color: var(--brand, rebeccapurple);
  }
}

@layer defaults {
  /* higher specificity, but lower layer priority */
  a:any-link { color: maroon; }
}

/* un-layered styles have the highest priority */
a {
  color: mediumvioletred;
}

Введение. Что такое «каскадные слои»?

Каскадные слои созданы решать сложные проблемы CSS. Рассмотрим главную проблему её решение с помощью каскадных слоёв.

Проблема: разрастание конфликтов специфичности

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

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

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

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

Иными словами, это то, насколько специфичен селектор. И это хорошее предположение, но правило не абсолютно надёжное, что чревато проблемами:

  • Выбор элементов сочетается с определением приоритета наборов правил.

  • Самый простой способ «исправить» конфликт со специфичностью — усугубить проблему, добавив ненужные в иных случаях селекторы, или написать !important:

.overly#powerful .framework.widget {
  color: maroon;
}

.my-single_class { /* add some IDs to this ??? */
  color: rebeccapurple; /* add !important ??? */
}

Решение: каскадные слои обеспечивают контроль

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

С @layer и многослойными @import можно задавать собственные слои каскада, формируя стили от низкоприоритетных, например resets и defaults через themes, frameworks и design systems, до высокоприоритетных, таких как components, utilities и overrides.

При этом специфичность применяется к конфликтам внутри каждого слоя, но конфликты слоёв всегда разрешаются в пользу слоя с приоритетом выше:

@layer framework {
  .overly#powerful .framework.widget {
    color: maroon;
  }
}

@layer site {
  .my-single_class {
    color: rebeccapurple;
  }
}

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

И, в отличие от важности, слои не двоичны: они переходят сразу на вершину стека или имеют нумерацию, подобную z-index, где нужно угадывать большое число (z-index: 9999999?). На самом деле многослойные стили по умолчанию менее важны, чем стили без слоёв:

@layer defaults {
  a:any-link { color: maroon; }
}

/* un-layered styles have the highest priority */
a {
  color: mediumvioletred;
}

Где слои располагаются в каскаде?

Каскад — это алгоритм разрешения конфликтов стилей:

html { --button: teal; }
button { background: rebeccapurple !important; }
.warning { background: maroon; }
<button class="warning" style="background: var(--button);">
  what color background?
</button>

Вот этапы алгоритма с учётом каскадных слоёв:

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

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

Каскадные слои повышают значимость полного понимания роли !important в каскаде — не только как инструмента для «повышения специфичности», но и как системы балансировки задач.

Источники, контекст и слои !important располагаются в противоположном порядке

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

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

Источники важности

Всё начинается с источников, откуда в веб-экосистеме берётся стиль. В CSS основных источника три. Это:

  • браузер (или пользовательский агент);

  • пользователь (часто через настройки браузера);

  • веб-авторы (это мы!).

В браузерах для всех элементов предоставляются читаемые значения по умолчанию, затем пользователи устанавливают свои предпочтения, и уже мы (авторы) предоставляем дизайн веб-страниц. Итак, изначально у браузеров самый низкий приоритет, значения по умолчанию в них переопределяются пользовательскими предпочтениями, а мы можем переопределить их все. Но создатели CSS очень чётко дали понять, что на самом деле последнее слово не должно оставаться за нами:

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

— Хокон Ли (выделено мной)

Таким образом, важность даёт браузеру и пользователям возможность вернуть свой приоритет, когда это наиболее важно. Когда к стилю добавляется флаг !important, создаются три новых слоя, а порядок становится обратным!

1) стили браузера !important (самые приоритетные);

2) пользовательские предпочтения !important;

3) авторские стили !important;

4) обычные авторские стили;

5) обычные пользовательские предпочтения;

6) обычные стили браузера (наименее приоритетные).

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

iframe:fullscreen {
  /* iframes in full-screen mode don't show a border. */
  border: none !important;
  padding: unset !important;
}

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

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

Важный контекст

Та же базовая логика применяется к контексту в каскаде. По умолчанию стили встроенного контекста (теневой DOM) переопределяются стилями главного документа (обычный DOM). Но при добавлении !important порядок меняется на противоположный:

  1. Теневой контекст !important (самый приоритетный).

  2. Главный контекст !important.

  3. Обычный главный контекст.

  4. Обычный теневой контекст (наименее приоритетный).

Определённые в главном документе важные стили переопределяются важными стилями теневого контекста.

Вот пользовательский элемент odd-bird. Одни его стили написаны в шаблоне элемента (теневой DOM), другие — в таблице стилей главной страницы (обычный DOM):

У обоих объявлений color важность обычная, поэтому главная страница mediumvioletred обладает приоритетом. Но объявления font-family помечены как !important, что даёт преимущество теневому контексту, где определяется fantasy.

Важные слои

Принцип работы каскадных слоёв тот же, что у источников и контекста, причём важные слои располагаются в обратном порядке. Единственная разница заключается в том, что это поведение гораздо более заметно из-за слоёв.

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

Каскадные слои настраиваются, поэтому предопределённого порядка нет. Но начать можно с трёх слоёв:

1) utilities (самый приоритетный);

2) components (среднего приоритета);

3) defaults (наименее приоритетный).

Когда стили в этих слоях отмечаются как важные, в них создаются три новых слоя, отмеченных !important, в обратном порядке:

  1. !important defaults (самый приоритетный).

  2. !important components.

  3. !important utilities.

  4. Обычные utilities.

  5. Обычные components.

  6. Обычные defaults (наименее приоритетный).

В этом примере цвет определяется во всех трёх обычных слоях; за счёт применения цвета maroon конфликт разрешается в пользу utilities: именно у него приоритет в @layer выше. Обратите внимание: text-decoration отмечено как !important в слоях defaults и components, где приоритетом обладают важные defaults, которые применяют объявленное в слое defaults подчёркивание:

Установление порядка слоёв

Можно создать любое количество слоёв и называть или группировать их различными способами. Но самое важное — убедиться, что слои применяются в правильном порядке.

Один слой в кодовой базе может использоваться несколько раз (в стеке каскадных слоёв) в том порядке первого появления. Первый встретившийся слой находится внизу, он наименее приоритетный, а последний — вверху, он самый приоритетный. Но тогда высшим приоритетом обладают стили без слоёв:

@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 { a { color: yellow; } }
/* un-layered */ a { color: green; }
  1. Стили без слоёв (самые приоритетные).

  2. layer-3.

  3. layer-2.

  4. layer-1 (наименее приоритетный).

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

@layer layer-1 { a { color: red !important; } }
@layer layer-2 { a { color: orange !important; } }
@layer layer-3 { a { color: yellow !important; } }
/* un-layered */ a { color: green !important; }
  1. layer-1 !important (самый приоритетный).

  2. layer-2 !important.

  3. layer-3!important.

  4. Стили без слоёв !important.

  5. Обычные стили без слоёв.

  6. Обычный layer-3.

  7. Обычный layer-2.

  8. Обычный layer-1 (наименее приоритетный).

Слои можно группировать. Это позволяет выполнять сложную сортировку слоёв верхнего уровня и вложенных слоёв:

@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 {
  @layer sub-layer-1 { a { color: yellow; } }
  @layer sub-layer-2 { a { color: green; } }
  /* un-nested */ a { color: blue; }
}
/* un-layered */ a { color: indigo; }
  1. Стили без слоёв (самые приоритетные).

  2. layer-3:

  1. layer-3 невложенный.

  2. layer-3, sub-layer-2.

  3. layer-3, sub-layer-1.

  1. layer-2.

  2. layer-1 (наименее приоритетный).

Сгруппированные слои в окончательном порядке слоёв всегда остаются вместе (например, все подслои layer-3 окажутся рядом друг с другом). Но в остальном здесь поведение такое же, как если бы список был «упрощённым», т. е. единым списком из шести пунктов. Когда меняется порядок слоёв !important, порядок всего упрощённого списка становится обратным.

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

/* describe the layer in one place */
@layer my-layer;

/* append styles to it from anywhere */
@layer my-layer { a { color: red; } }

В одном объявлении мы даже можем определять целый упорядоченный список слоёв:

@layer one, two, three, four, five, etc;

Это позволяет автору сайта в отношении порядка слоёв оставлять последнее слово за собой. Можно указать порядок слоёв заранее, то есть до импорта любого стороннего кода, установить и изменить порядок в одном месте, не беспокоясь об использовании слоёв в стороннем инструменте.

Синтаксис: работа с каскадными слоями

Перейдём к синтаксису.

Инструкции, чтобы задать порядок @layer

Слои располагаются в порядке их определения. Поэтому важно иметь инструмент, задающий порядок в одном месте. Здесь можно воспользоваться инструкцией @layer:

@layer <layer-name>#;

Решётка означает, что в список со значениями мы можем добавить столько угодно разделёнными запятыми имён слоёв:

@layer reset, defaults, framework, components, utilities;

Их порядок станет таким:

  1. стили без слоёв (самые приоритетные);

  2. utilities;

  3. components;

  4. framework;

  5. defaults;

  6. reset (наименее приоритетный).

Можно добавлять сколько угодно имён, не забывая, что важен порядок их первого появления. Результат будет тем же:

@layer reset, defaults, framework, components, utilities;

В логике упорядочения порядок reset, defaults и framework во втором @layer будет проигнорирован, ведь эти слои уже установлены. С этим синтаксисом списка @layer в логику упорядочения слоёв не добавляется никакой особой магии: слои располагаются согласно порядку появления в коде.

Первым здесь появляется reset первого списка @layer. Все следующие @layer могут лишь добавлять имена слоёв в список — уже имеющиеся слои с их помощью не переместятся. Это гарантия, что вы всегда сможете контролировать окончательный общий порядок слоёв из одного места, то есть в самом начале стилей.

Инструкции упорядочивания слоёв разрешены наверху таблицы стилей, перед правилом @import, но не между импортами. Мы очень рекомендуем использовать этот функционал, чтобы заранее устанавливать все слои в одном месте: так вы всегда будете знать, где искать или вносить изменения.

Блочный @layer

В блочной версии правила @layer принимается только одно имя слоя, но затем к этому слою можно добавлять стили:

@layer <layer-name> {
  /* styles added to the layer */
}

Поместить в блок @layer можно многое — это мультимедийные запросы, селекторы, стили, запросы @support и т. д. Нельзя поместить туда набор символов, импорты и пространства имён. Зато есть синтаксис импортирования стилей в слой.

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

Если заранее установить порядок слоёв с помощью выражения @layer, то беспокоиться о порядке блоков слоёв больше не нужно:

/* establish the order up-front */
@layer defaults, components, utilities;

/* add styles to layers in any order */
@layer utilities {
  [hidden] { display: none; }
}

/* utilities will override defaults, based on established order */
@layer defaults {
  * { box-sizing: border-box; }
  img { display: block; }
}

Группировка (вложенных) слоёв

Слои можно группировать по правилам их вложенности:

@layer one {
  /* sorting the sub-layers */
  @layer two, three;

  /* styles ... */
  @layer three { /* styles ... */ }
  @layer two { /* styles ... */ }
}

Так генерируются сгруппированные слои, которые можно представить как объединение родительского и дочернего имён с точкой. Это означает, что прямой доступ к подслоям можно получить за пределами группы:

/* sorting nested layers directly */
@layer one.two, one.three;

/* adding to nested layers directly */
@layer one.three { /* ... */ }
@layer one.two { /* ... */ }

Правила упорядочения слоёв применяются на каждом уровне вложенности. Любые стили, которые не вложены глубже, в этом контексте считаются «стилями без слоёв», а значит, обладают приоритетом над более глубокими слоями:

@layer defaults {
  /* un-layered defaults (higher priority) */
  :any-link { color: rebeccapurple; }

  /* layered defaults (lower priority) */
  @layer reset {
    a[href] { color: blue; }
  }
}

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

@layer reset.type, default.type, reset.media, default.media;

В результате получается следующий порядок слоёв:

  • без слоёв (самый приоритетный);

  • группа default:

  • default без слоёв;

  • default.media;

  • default.type;

  • группа reset:

  • reset без слоёв;

  • reset.media;

  • reset.type.

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

Такая группировка приобретает особую важность в применении @import или <link> к целым таблицам стилей слоёв. В сторонних инструментах типа Bootstrap внутри могут использоваться слои. Но эти слои можно вложить в импорте в общую группу слоёв bootstrap, чтобы избежать потенциальных конфликтов их именования.

Наложение слоёв целых таблиц стилей через @import или <link>

Целые таблицы стилей к слою можно добавить с помощью нового синтаксиса функции layer() с правилами @import:

/* styles imported into to the <layer-name> layer */
@import url('example.css') layer(<layer-name>);

Есть предложение добавить атрибут layer в элемент HTML <link>, хотя оно дорабатывается и пока нигде не поддерживается. Использовать его можно для импорта сторонних инструментов или библиотек компонентов, объединяя любые внутренние слои под именем одного слоя, или как способ организации слоёв в отдельные файлы.

Анонимные (безымянные) слои

Имена слоёв позволяют получить доступ к конкретному слою из нескольких мест, чтобы сортировать или комбинировать блоки слоёв, но они необязательны. Блочное @layer позволяет создавать безымянные (анонимные) слои.

@layer { /* ... */ }
@layer { /* ... */ }

Ещё вариант — это синтаксис импорта с ключевым словом layer вместо функции layer():

/* styles imported into to a new anonymous layer */
@import url('../example.css') layer;

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

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

  • Сторонние инструменты могли бы «скрывать» своё внутреннее наложение слоёв внутри анонимных слоёв, чтобы внутреннее наложение не становились частью открытого API.

Возврат значений к предыдущему слою

Есть несколько способов «вернуть» стиль в каскаде к предыдущему значению, определяемому источником или слоем с более низким приоритетом, включая ряд глобальных значений CSS и новое ключевое слово revert-layer, тоже глобальное, то есть работающее в любом свойстве.

Контекст: имеющиеся глобальные ключевые слова каскада

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

  • С initial свойство устанавливается в указанное значение до применения любых стилей, включая браузерные значения по умолчанию. Это может удивить, ведь исходными значениями часто считают стили браузера. Но значение initial, например, в display является inline (встроенным), в каком бы элементе оно ни использовалось.

  • С inherit свойство устанавливается в значение свойства родительского элемента. Для наследуемых свойств это значение по умолчанию, однако его можно использовать для удаления предыдущего значения.

  • С unset все предыдущие значения как будто просто удаляются: наследуемые свойства снова применяют inherit (наследуются), а ненаследуемые возвращаются к initial (первоначальному) значению.

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

Новое ключевое слово revert-layer

В каскадные слои добавляется новое глобальное ключевое слово revert-layer. Оно отличается от revert тем, что с его помощью удаляются только те значения, применяемые в текущем каскадном слое. Использовать его можно для отката каскада, применимо любое определённое в предыдущих слоях значение.

Ниже с помощью класса .no-theme удаляются любые значения, установленные в @layer theme:

@layer default {
  a { color: maroon; }
}

@layer theme {
  a { color: var(--brand-primary, purple); }

  .no-theme {
    color: revert-layer;
  }
}

Тег link с классом .no-theme вернётся к применению значения, установленного в слое default. В стилях без слоёв поведение revert-layer будет таким же, как revert: откат к предыдущему источнику.

Возврат важных слоёв

Самое интересное начинается при добавлении !important к revert-layer. У каждого слоя есть два различных положения в каскаде: «обычное» и «важное». Поэтому меняется не просто приоритет объявления, но и то, какие слои возвращаются.

Допустим, в стеке слоёв есть три определённых слоя:

  1. utilities (самый приоритетный);

  2. components;

  3. defaults (наименее приоритетный).

Включим в него обычные и важные положения каждого слоя, а также анимацию и стили без слоёв:

  1. !important defaults (самый приоритетный).

  2. !important components.

  3. !important utilities.

  4. Стили без слоёв !important.

  5. CSS-анимация.

  6. Обычные стили без слоёв.

  7. Обычные utilities.

  8. Обычные components.

  9. Обычные defaults (наименее приоритетный).

При использовании revert-layer в обычном слое (например, utilities) результат довольно простой. Возвращается только этот слой, а всё остальное будет как обычно:

  1. ✅ !important defaults (самый приоритетный).

  2. ✅ !important components.

  3. ✅ !important utilities.

  4. ✅ Стили без слоёв !important.

  5. ✅ CSS-анимация.

  6. ✅ Обычные стили без слоёв.

  7. ❌ Обычные utilities.

  8. ✅ Обычные components.

  9. ✅ Обычные defaults (наименее приоритетный).

Когда этот revert-layer перемещается в важное положение, возвращаются обе версии (обычная и важная) и всё, что между ними:

  1. ✅ !important defaults (самый приоритетный).

  2. ✅ !important components.

  3. ❌ !important utilities.

  4. ❌ Стили без слоёв !important.

  5. ❌ CSS-анимация.

  6. ❌ Обычные стили без слоёв.

  7. ❌ Обычные utilities.

  8. ✅ Обычные components.

  9. ✅ Обычные defaults (наименее приоритетный).

Когда (не) применять каскадные слои?

Приведём примеры того, когда каскадные слои кстати, а когда большого смысла в них нет.

Не такие надоедливые resets и defaults

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

  • его нужно применять к каждому селектору отдельно;

  • конфликты внутри reset должны разрешаться без специфичности.

Слои позволяют проще оборачивать всю таблицу стилей reset с помощью блочного @layer:

/* reset.css */
@layer reset {
  /* all reset styles in here */
}

Или импорта reset:

/* reset.css */
@import url(reset.css) layer(reset);

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

Приоритет многослойных стилей ниже, чем стилей по умолчанию «без слоёв». А значит, можно начать использовать каскадные слои, не переписывая всю кодовую базу CSS.

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

Управление CSS сложной архитектуры

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

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

Фактически первым шагом для добавления слоёв в каскад CSS была методология ITCSS как основной пример и руководство по разработке функции.

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

Например:

  1. Низкоуровневый сброс и стили нормализации.

  2. Значения элементов по умолчанию для базовой типографики и удобочитаемости.

  3. Темы, светлый и тёмный режимы.

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

  5. Макеты и большие структуры страниц.

  6. Отдельные компоненты.

  7. Переопределения и утилиты.

Этот стек слоёв верхнего уровня можно написать в самом начале CSS одним @layer:

@layer
  reset,
  default,
  themes,
  patterns,
  layouts,
  components,
  utilities;

Нужны взыскательные слои: их названия могут меняться от проекта к проекту. Создадим ещё более подробные разбивки слоёв. Возможно, внутри самих components есть defaults, structures, themes и utilities:

@layer components {
  @layer defaults, structures, themes, utilities;
}

Теперь можно дальше разделять стили на слои внутри каждого компонента, не меняя структуру верхнего уровня, .

Использование сторонних инструментов и фреймворков

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

Не важно, что применяется: общий сброс (Normalizer или CSS Remedy), универсальная дизайн-система вроде Material Design, фреймворк типа Bootstrap или набор служебных инструментов Tailwind. Иногда это верно даже для внутренних библиотек, дизайн-систем и инструментов других частей организации.

В итоге часто приходится структурировать внутренний CSS вокруг стороннего кода или расширять появляющиеся конфликты, искусственно повышая специфичность или применяя флаги !important. Эти «костыли» нужно поддерживать, адаптируя к изменениям.

Каскадные слои дают возможность встроить сторонний код в каскад любого проекта именно там, где он нужен, — не важно, как селекторы написаны внутри. Сделать это можно разными способами в зависимости от типа используемой библиотеки. Продвигаясь от resets к utilities, начнём с базового стека слоёв:

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

Затем можно подключить инструменты…

Использование сброса

Можно включить собственные стили сброса инструментом типа CSS Remedy, для этого импортируем CSS Remedy в подслой reset:

@import url('remedy.css') layer(reset.remedy);

Теперь собственные стили сброса можно добавить в слой reset без какой-либо дополнительной вложенности, если она не нужна. Любые вложенные стили переопределяются прямо в reset, поэтому мы можем быть уверены, что в случае конфликта наши стили всегда будут иметь приоритет, какими бы ни были изменения в новой версии CSS Remedy:

@import url('remedy.css') layer(reset.remedy);

@layer reset {
  :is(ol, ul)[role='list'] {
    list-style: none;
    padding-inline-start: 0;
  }
}

А поскольку слой reset находится внизу стека, в остальной части CSS нашей системы будут переопределены и Remedy, и собственные локальные дополнения reset.

Использование служебных классов

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

Эти классы имеют свойство нарушать эвристические подходы специфичности, ведь нам нужно, чтобы они определялись широко, что ведёт к низкой специфичности. А ещё обычно нужно, чтобы служебные классы «выигрывали» конфликты. Такое возможно благодаря слою utilities вверху стека. Мы загружаем в подслой внешние и собственные utilities, как в примере с reset:

@import url('tailwind.css') layer(utilities.tailwind);

@layer utilities {
  /* from https://kittygiraudel.com/snippets/sr-only-class/ */
  /* but with !important removed from the properties */
  .sr-only {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    -webkit-clip-path: inset(50%);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    margin: -1px;
    padding: 0;
    position: absolute;
    width: 1px;
    white-space: nowrap;
  }
}

Использование дизайн-систем и библиотек компонентов

Много инструментов CSS находится где-то в середине стека слоёв, объединяя типографские настройки по умолчанию, темы, компоненты и другие аспекты системы.

В зависимости от конкретного инструмента мы могли бы сделать что-то похожее на приведённые выше примеры с reset и utility, но есть другие варианты. Инструмент с тесной интеграцией может заслуживать отдельного слоя верхнего уровня:

@layer reset, bootstrap, utilities;
@import url('bootstrap.css') layer(bootstrap);

Если в этих инструментах слои начнут предоставлять как часть их общедоступного API, этот API также можно разбить на части, что позволит чередовать код с библиотекой:

@import url('bootstrap/reset.css') layer(reset.bootstrap);
@import url('bootstrap/theme.css') layer(theme.bootstrap);
@import url('bootstrap/components.css') layer(components.bootstrap);
@layer theme.local {
/* styles here will override theme.bootstrap /
/ but not interfere with styles from components.bootstrap */
}

Использование слоёв с фреймворками

Как при любом серьёзном изменении языка, наступит период адаптации, когда каскадные слои CSS получат широкое применение. Что, если ваша команда готова начать использовать слои уже в следующем месяце, а любимому фреймворку для перехода на многослойные стили нужно ещё три года? Скорее всего, во многих фреймворках !important по-прежнему будут использоваться чаще, чем хотелось бы! А с обратным порядком слоёв !important это неидеально.

Но слои всё равно могут помочь решить эту проблему. Нужно просто подойти к решению с умом. То, какие слои нужны для проекта, определяем мы. Можно добавлять слои выше и ниже созданных нами слоёв фреймворка. А пока можно использовать слой ниже для переопределения !important стилей из фреймворка, а слой выше — для переопределения обычных стилей. Что-то вроде этого:

@layer framework.important, framework.bootstrap, framework.local;
@import url('bootstrap.css') layer(framework.bootstrap);

@layer framework.local {
  /* most of our normal framework overrides can live here */
}

@layer framework.important {
  /* add !important styles in a lower layer */
  /* to override any !important framework styles */
}

Всё ещё похоже на «костыли», но они помогают двигаться в правильном направлении, к более структурированному каскаду. Надеюсь, это временное решение.

Разработка инструмента или фреймворка CSS

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

Например, в Bootstrap могут предоставляться слои для reboot, grid и utilities; вероятно, в таком порядке. Пользователь сможет решить, нужно ли ему загружать эти слои Bootstrap в разные локальные слои:

@import url(bootstrap/reboot.css) layer(reset); /* reboot » reset.reboot */
@import url(bootstrap/grid.css) layer(layout); /* grid » layout.grid */
@import url(bootstrap/utils.css) layer(override); /* utils » override.utils */

Или загрузить их в слой Bootstrap с чередованием локальных слоёв:

@layer bs.reboot, bs.grid, bs.grid-overrides, bs.utils, bs.util-overrides;
@import url('bootstrap-all.css') layer(bs);

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

Мне просто нужно свойство, чтобы стать более !important (важным)

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

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

Слои должны быть более фундаментальными, устанавливающими в проекте последовательные шаблоны, а не стиль за стилем. Если мы всё настроили правильно, то в идеале получим подходящий результат, переместив стиль в соответствующий, заранее определённый слой.

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

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

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

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

Мы могли бы сказать, что добавление display: none к атрибуту [hidden] подходит в reset с самым низким приоритетом, но всё равно этот стиль должно быть трудно переопределить. В данном случае !important — действительно правильный инструмент для такой работы:

@layer reset {
  [hidden] { display: none !important; }
}

Стили области видимости и пространств имён? Нет!

Безусловно, каскадные слои — это организационный инструмент, который «улавливает» влияние селекторов, особенно когда они конфликтуют. Поэтому сначала может возникнуть соблазн посчитать их решением для управления областью видимости или пространством имён и создать слой для каждого компонента в проекте в надежде, что это обеспечит, например, применение .post-title только внутри .post.

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

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

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

В областях видимости описывается, что мы стилизуем, а в слоях — почему. Кроме того, слои дают представление о том, откуда берётся стиль, а области видимости — о том, к чему он будет привязан.

Проверьте свои знания: какой стиль в приоритете?

В каждой ситуации рассматривается абзац:

<p id="intro">Hello, World!</p>

Вопрос 1

@layer ultra-high-priority {
  #intro {
    color: red;
  }
}

p {
  color: green;
}
Какого цвета абзац?

Несмотря на имя, которое выглядит довольно важным, выше приоритет стилей без слоя. Абзац будет зелёным.

Вопрос 2

@layer ren, stimpy;

@layer ren {
  p { color: red !important; }
}

p { color: green; }

@layer stimpy {
  p { color: blue !important; }
}
Какого цвета абзац?

Вначале устанавливается нормальный порядок: внизу ren, затем stimpy и в верхней части (как всегда) стили без слоёв. Но не все эти стили нормальные, некоторые из них имеют !important. Сразу можно отфильтровать только !important, а также игнорировать green. Помните, что «источник и важность» — первые шаги каскада, они учитываются до работы со слоями.

Останется два стиля c !important, оба в слоях. Поскольку слои с !important разворачиваются, ren движется к вершине, а stimuli — вниз. Абзац будет красным.

Вопрос 3

@layer Montagues, Capulets, Verona;

@layer Montagues.Romeo { #intro { color: red; } }
@layer Montagues.Benvolio { p { color: orange; } }

@layer Capulets.Juliet { p { color: yellow; } }
@layer Verona { * { color: blue; } }
@layer Capulets.Tybalt { #intro { color: green; } }
Какого цвета абзац

Все в пределах одного источника и контекста, !important не помечен ни один, нет и встроенных стилей. Есть много разных селекторов, от самого специфичного ID #intro до универсального селектора. Но слои разрешаются до учёта специфики, поэтому сейчас селекторы можно игнорировать.

Есть первичный слой, а в нём подслои, которые сортируются вместе с родительским слоем, а значит, все Montagues будут обладать самым низким приоритетом, затем идут все Capulets. Последнее слово за Verona. Можно сразу отфильтровать только стили Verona, которые имеют приоритет. И хотя специфичность селектора * нулевая, в конфликте он одержит верх.

Универсальные селекторы в приоритетных слоях располагайте осторожно!

Отладка конфликтов слоёв в инструментах разработчика

В браузерах Chrome, Safari, Firefox и Edge есть инструменты разработчика, позволяющие просматривать стили элемента страницы. В палитре стилей этого инспектора элементов показываются применяемые селекторы, отсортированные по их каскадному приоритету (наивысший — вверху), а ниже — унаследованные стили. Стили, которые по какой-то причине не применяются, обычно выделяются серым цветом или даже перечёркиваются — иногда с дополнительной информацией о том, почему стиль не применяется. Это первое, куда нужно смотреть при отладке любого аспекта каскада, включая конфликты слоёв.

С помощью Safari Technology Preview и Firefox Nightly в данной палитре уже показываются и сортируются каскадные слои. Ожидается, что этот инструментарий выкатят в стабильных версиях одновременно с каскадными слоями. Слой каждого селектора указан прямо над ним:

В Safari:

Firefox, Chrome/Edge работают над аналогичными инструментами, появление которых ожидается в nightly-версиях Canary к тому времени, когда каскадные слои будут в стабильной версии. Мы будем обновлять статью по мере изменений в инструментах.

Поддержка @layer браузерами и резервные варианты

Каскадные слои доступны или скоро будут доступны по умолчанию во всех трёх основных движках браузеров:

  • Chrome/Edge 99+;

  • Firefox 97+;

  • Safari (сейчас Safari Technology Preview).

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

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

Поддержка функции запроса с помощью @supports

Функция @supports в CSS позволяет авторам проверять поддержку @layer и других правил с @:

@supports at-rule(@layer) {
  /* code applied for browsers with layer support */
}

@supports not at-rule(@layer) {
  /* fallback applied for browsers without layer support */
}

Однако неясно, когда сам этот запрос будет поддерживаться в браузерах.

Назначение слоёв в HTML с помощью тега <link>

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

<!-- styles imported into to the <layer-name> layer -->
<link rel="stylesheet" href="example.css" layer="<layer-name>">

<!-- styles imported into to a new anonymous layer -->
<link rel="stylesheet" href="example.css" layer>

Но в старых браузерах без поддержки атрибута layer он будет полностью игнорироваться: таблица стилей продолжит загружаться без наложения слоёв и результаты могут оказаться довольно неожиданными.

В предложении также расширяется атрибут media, он будет выполнять запросы поддержки функционала в support(). Это позволило бы делать многослойные ссылки условными, основываясь на поддержке @layer:

<link rel="stylesheet" layer="bootstrap" media="supports(at-rule(@layer))" href="bootstrap.css">

Потенциальные полифиллы и обходные решения

Все основные браузеры перешли на «вечнозелёную» модель с обновлениями, предоставляемыми пользователям в довольно коротком цикле выпуска. Даже для Safari регулярно выпускаются новые функции в «запатченных» обновлениях между их основными версиями, которые кажутся более редкими.

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

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

Ещё материалы

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

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

Выбрать другую востребованную профессию.

Ссылки из статьи
Краткий каталог курсов и профессий

Комментарии (3)


  1. mSnus
    10.03.2022 02:13
    +3

    Теперь отловить неиспользуемые стили будет ещё сложнее...


  1. ChiDa
    10.03.2022 09:21
    +1

    Очень много текста. Мне кажется, можно было проще написать


  1. d1gital_love
    10.03.2022 09:24

    Не обязательно повторять за автором ошибка в ошибку при переводе.

    Chromium проект(ы) называется(ются).