Перевод статьи Don’t Fight the Cascade, Control It!
с сайта css-tricks.com для css-live.ru. Автор — Мадс Стуманн.
Если делать всё правильно и использовать наследование, которое даёт CSS-каскад, то конечного CSS нужно будет писать меньше. Но поскольку часто мы загружаем CSS из разных мест — из-за чего его бывает сложно структурировать и поддерживать, — каскад может сильно расстроить, и CSS из-за него окажется больше, чем нужно.
Несколько лет назад Гарри Робертс придумал ITCSS — умный способ структурировать CSS.
В сочетании с БЭМ ITCSS стал популярным способом написания и организации CSS.
Но даже с ITCSS и БЭМ иногда возникают большие трудности с каскадом. К примеру, я уверен, что вам приходилось делать @import
внешних CSS-компонентов в определённом месте, чтобы ничего не сломать, или прибегать к жуткому !important
.
Недавно в CSS были добавлены новые инструменты, позволяющие, наконец, управлять каскадом. Давайте на них посмотрим.
О каскад, :where
же ты?
Псевдоселектор :where
позволяет уменьшить специфичность до «аккурат после дефолтных браузерных стилей», независимо от того, где и когда в документ загружается CSS. Это означает, что специфичность всех аргументов в :where буквально равна нулю — она полностью удаляется. Это удобно для универсальных компонентов, о которых мы поговорим чуть позже.
Для начала, представьте какие-то общие стили <table>
, с использованием :where:
:where(table) { background-color: tan; }
Теперь, если добавить другие стили таблицы перед селектором :where
:
table { background-color: hotpink; } :where(table) { background-color: tan; }
… то фон таблицы становится ярко-розовым, несмотря на то, что в каскаде селектор таблицы указан перед селектором :where
. В этом прелесть :where
, и вот почему его уже применяют в CSS-сбросах.
У :where
есть близнец, действие которого почти противоположно, это селектор :is
Специфичность псевдокласса
:is()
определяется его наиболее специфичным аргументом. Таким образом, если есть два одинаковых селектора, один из которых написан с помощью:is()
, а другой нет, то их специфичность не обязательно должна быть равной. Спецификация селекторов 4 уровня.
Расширяя наш предыдущий пример:
:is(table) { --tbl-bgc: orange; } table { --tbl-bgc: tan; } :where(table) { --tbl-bgc: hotpink; background-color: var(--tbl-bgc); }
Цвет фона <table class="c-tbl">
будет рыжевато-коричневым, потому что специфичность у :is такая же, как у table
, но table
идет позже.
See the Pen
Untitled by Geoff Graham (@geoffgraham)
on CodePen.
Однако, если заменить его на это:
:is(table, .c-tbl) { --tbl-bgc: orange; }
… то цвет фона будет оранжевым, поскольку у :is
будет вес самого тяжелого селектора в нем, то есть .c-tbl
.
See the Pen
Untitled by Geoff Graham (@geoffgraham)
on CodePen.
Пример: настраиваемый компонент таблицы
А теперь посмотрим, как можно использовать :where
в наших компонентах. Создадим компонент таблицы, начиная с HTML:
See the Pen
Untitled by CSS-Tricks (@css-tricks)
on CodePen.
Давайте обернём .c-tbl
в селектор where и, просто по приколу, добавим таблице загруглённые углы. Это значит, что нам нужен border-collapse: separate
, поскольку мы не можем использовать border-radius
для ячеек таблицы с border-collapse: collapse
:
:where(.c-tbl) { border-collapse: separate; border-spacing: 0; table-layout: auto; width: 99.9%; }
Эти ячейки используют разную стилизацию для ячеек в <thead>
и <tbody>
:
:where(.c-tbl thead th) { background-color: hsl(200, 60%, 40%); border-style: solid; border-block-start-width: 0; border-inline-end-width: 1px; border-block-end-width: 0; border-inline-start-width: 0; color: hsl(200, 60%, 99%); padding-block: 1.25ch; padding-inline: 2ch; text-transform: uppercase; } :where(.c-tbl tbody td) { background-color: #FFF; border-color: hsl(200, 60%, 80%); border-style: solid; border-block-start-width: 0; border-inline-end-width: 1px; border-block-end-width: 1px; border-inline-start-width: 0; padding-block: 1.25ch; padding-inline: 2ch; }
И из-за наших закруглённых углов и недостающего border-collapse: collapse
нам нужно добавить несколько дополнительных стилей, а именно для рамок таблицы и состояния наведения на ячейки:
::where(.c-tbl tr td:first-of-type) { border-inline-start-width: 1px; } :where(.c-tbl tr th:last-of-type) { border-inline-color: hsl(200, 60%, 40%); } :where(.c-tbl tr th:first-of-type) { border-inline-start-color: hsl(200, 60%, 40%); } :where(.c-tbl thead th:first-of-type) { border-start-start-radius: 0.5rem; } :where(.c-tbl thead th:last-of-type) { border-start-end-radius: 0.5rem; } :where(.c-tbl tbody tr:last-of-type td:first-of-type) { border-end-start-radius: 0.5rem; } :where(.c-tbl tr:last-of-type td:last-of-type) { border-end-end-radius: 0.5rem; } /* hover */ @media (hover: hover) { :where(.c-tbl) tr:hover td { background-color: hsl(200, 60%, 95%); } }
See the Pen
Basic table with rounded corners [article] by Mads Stoumann (@stoumann)
on CodePen.
Теперь можно создавать варианты нашего компонента таблицы, вставляя стили без :where ниже или выше наших общих стилей. Двумя способами: либо перезаписывая элемент .c-tbl
, либо добавляя класс-модификатор в стиле БЭМ (к примеру, класс c-tbl--purple
):
<table class="c-tbl c-tbl--purple">
.c-tbl--purple th { background-color: hsl(330, 50%, 40%) } .c-tbl--purple td { border-color: hsl(330, 40%, 80%); } .c-tbl--purple tr th:last-of-type { border-inline-color: hsl(330, 50%, 40%); } .c-tbl--purple tr th:first-of-type { border-inline-start-color: hsl(330, 50%, 40%); }
See the Pen
Basic table with rounded corners [variation] by CSS-Tricks (@css-tricks)
on CodePen.
Круто! Но вы заметили, как мы повторяем цвета снова и снова? А что, если понадобится изменить радиус или ширину рамки? Это закончилось бы кучей повторяющегося CSS.
Давайте перенесём всё это в кастомные CSS-свойства, и пока мы это делаем, можно перенести все настраиваемые свойства на верхний уровень «области видимости» компонента — сам элемент таблицы — чтобы потом было легче делать с ними что угодно.
Кастомные CSS-свойства
Я собираюсь изменить HTML и использовать атрибут data-component для элемента table, на который можно натравить стили.
<table data-component="table" id="table">
В этом data-component
будут находиться общие стили, которые можно использовать в любом экземпляре компонента, то есть стили, нужные таблице независимо от цветовой вариации, которую мы используем. Стили для конкретного экземпляра компонента таблицы будут находиться в обычном классе, используя кастомные свойства из универсального компонента.
[data-component="table"] { /* Стили, которые нужны для всех вариаций таблицы } .c-tbl--purple { /* Стили для фиолетовой вариации*/ }
Если мы поместим все общие стили в data-атрибут, то сможем использовать любую систему именования, какую захотим. Таким образом, не нужно беспокоиться, если ваш начальник заставляет вас называть классы таблицы .BIGCORP__TABLE
, .table-component
или как-то ещё.
Все CSS-свойства для базового компонента задаются через кастомные свойства. Те свойства, которые будут работать для вложенных элементов — например, border-color
— определяются на корневом уровне этого компонента.
:where([data-component="table"]) { /* Это будет использоваться множество раз, и в других селекторах */ --tbl-hue: 200; --tbl-sat: 50%; --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%); } /* Здесь, это используется на дочернем элементе */ :where([data-component="table"] td) { border-color: var(--tbl-bdc); }
Для других свойств решите, должны ли у них быть статические значения, или они должны настраиваться с помощью их собственного кастомного свойства. При использовании кастомных свойств не забудьте предусмотреть значение по умолчанию, к которому таблица сможет откатиться, если никакого класса-вариации не окажется.
:where([data-component="table"]) { /* Эти свойства опциональны, с фолбеком */ background-color: var(--tbl-bgc, transparent); border-collapse: var(--tbl-bdcl, separate); }
Если хотите знать, как я называю кастомные свойства, то я использую префикс компонента (к примеру,
--tbl
), за которым следует сокращение из Emmet (к примеру,-bgc
). В этом случае--tbl
— это префикс компонента, -bgc — цвет фона, а-bdcl
—border-collapse
. Так, к примеру,tbl-bgc
— это цвет фона компонента таблицы. Я использую эту систему именования только при работе со свойствами компонентов, в отличие от глобальных свойств, которые я предпочитаю использовать в более общем виде.
See the Pen
Basic table using CSS Props [article] by Mads Stoumann (@stoumann)
on CodePen.
Теперь, если мы откроем отладчик, то сможем поэкспериментировать с кастомными свойствами. К примеру, можно изменить --tbl-hue
на другое значение оттенка в цвете HSL, установить --tbl-bdrs: 0
, чтобы удалить border-radius
, и так далее.
При работе с вашими собственными компонентами на этом этапе вы увидите, какие параметры (то есть значения кастомных свойств) нужны компоненту, чтобы всё выглядело правильно.
Мы можем также использовать кастомные свойства, чтобы контролировать выравнивание и ширину колонки:
::where[data-component="table"] tr > *:nth-of-type(1)) { text-align: var(--ca1, initial); width: var(--cw1, initial); /* Повторить для второй и третьей колонки, или использовать SCSS-цикл ... */ }
В инструментах разработчика выберете таблицу и добавьте эти правила в селектор element.styles
:element.style { --ca2: center; /* Выровнять вторую колонку по центру*/ --ca3: right; /* Выровнять третью колонку по правому краю */ }
Теперь давайте создадим стили конкретного компонента с помощью обычного класса .c-tbl
(сокращение для «component-table», как его бы назвали в БЭМ). Давайте добавим этот класс в разметку таблицы.
<table class="c-tbl" data-component="table" id="table">
Теперь, давайте изменим значение --tbl-hue
в CSS, чтобы посмотреть, как это работает, прежде чем лезть в гущу всех этих свойств со значениями:
.c-tbl { --tbl-hue: 330; }
Заметьте, что нам нужно только обновить свойства, а не писать полностью новый CSS! Изменение одного-единственного свойства обновляет цвет таблицы — никаких новых классов или перекрывающих свойств ниже в каскаде.
Заметьте, как цвета границ тоже меняются. Это потому, что все цвета в таблице наследуются от переменной -tbl-hue
Можно написать более сложный селектор, но всё равно обновить одно свойство, чтобы получить что-то вроде раскраски «зеброй»:
.c-tbl tr:nth-child(even) td { --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%); }
И помните: не важно, где вы загружаете класс. Поскольку наши общие стили используют :where
, специфичность стирается, и любые кастомные стили для конкретного варианта будут применяться, где бы они ни использовались. В этом вся прелесть использования :where
, чтобы подчинить себе каскад!
И самое главное, можно создавать все виды компонентов таблицы из общих стилей с помощью нескольких строк CSS
Фиолетовая таблица с раскрашенными «зеброй» столбцами
Светлая таблица с параметром “noinlineborder” … который мы рассмотрим далее
Добавление параметров с другим data-атрибутом
Всё идёт нормально! Общий компонент таблицы очень прост. Но что, если для этого требуется что-то более близкое к реальным параметрам? Возможно, для таких вещей, как:
- строки и столбцы в «зебру»
- фиксируемые заголовок и столбец
- действия при наведении, например, для всей строки, отдельной ячейки и всего столбца
Можно было бы просто добавить классы-модификаторы в стиле БЭМ, но есть более эффективный способ: добавить ещё один data-атрибут. Возможно, data-param, который содержит такие параметры:
<table data-component="table" data-param="zebrarow stickyrow">
Затем в нашем CSS можно использовать селектор атрибутов для соответствия целому слову в списке параметров. К примеру, строки, раскрашенные «зеброй»:
[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td { --tbl-td-bgc: var(--tbl-zebra-bgc); }
Или столбцы, раскрашенные «зеброй»:
[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) { --tbl-td-bgc: var(--tbl-zebra-bgc); }
Давайте-ка вообще обалдеем и сделаем заголовок таблицы и первый столбец прилипшими:
[data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child { --tbl-td-bgc: var(--tbl-zebra-bgc); inset-inline-start: 0; position: sticky; } [data-component="table"][data-param~="stickyrow"] thead th { inset-block-start: -1px; position: sticky; }
Вот демонстрация, которая позволяет вам изменять один параметр за раз:
See the Pen
Basic table with params [article] by Mads Stoumann (@stoumann)
on CodePen.
Светлая тема по умолчанию в демо — это:
.c-tbl--light { --tbl-bdrs: 0; --tbl-sat: 15%; --tbl-th-bgc: #eee; --tbl-th-bdc: #eee; --tbl-th-c: #555; --tbl-th-tt: normal; }
… а где задан data-param
со значением noinlineborder
— это соответствует таким стилям:
[data-param~="noinlineborder"] thead tr > th { border-block-start-width: 0; border-inline-end-width: 0; border-block-end-width: var(--tbl-bdw); border-inline-start-width: 0; }
Знаю, мой способ стилизации и настройки общих компонентов через data-атрибуты может подойти не всем. Я привык делать так, но не стесняйтесь придерживаться любого метода, с которым вам удобнее работать, будь то класс-модификатор БЭМ или что-то ещё.
Подводя итог: освойте всю мощь управления каскадом, которую дают :where
и :is
. И по возможности, выстраивайте CSS так, чтобы при создании новых вариантов компонентов. приходилось писать как можно меньше нового CSS!
Каскадные слои
Последний инструмент для укрощения каскада, который я хочу рассмотреть, это «Каскадные слои». На момент написания этой статьи это экспериментальная функция, определённая в спецификации модуля каскада и наследования 5 уровня. К ней можно получить доступ в Safari или Chrome, включив флаг #enable-cascade-layers
.
Брамус Ван Дамм прекрасно резюмирует эту концепцию:
Истинная сила каскадных слоёв исходит из их уникального положения в каскаде: перед специфичностью селектора и порядком появления. Из-за этого нам не нужно беспокоиться ни о специфичности селектора CSS, используемого в других слоях, ни о порядке, в котором мы загружаем CSS в эти слои — это очень удобно для больших команд или при загрузке стороннего CSS.
Возможно, еще приятнее его иллюстрация, показывающая где каскадные слои попадают в каскад:
Автор: Брамус Ван Дамм
В начале этой статьи я упомянул ITCSS — способ укротить каскад, указав порядок загрузки общих стилей, компонентов, и т.д. Каскадные слои позволяют внедрять таблицу стилей в заданное место. Итак, упрощённая версия этой структуры в каскадных слоях выглядит так:
@layer generic, components;
С помощью этой единственной строки мы определили порядок наших слоёв. Сначала идут общие стили, а после стили, специфичные для компонентов.
Давайте представим, что мы загружаем наши общие стили намного позже, чем стили компонентов:
@layer components { body { background-color: lightseagreen; } } /* ГОРАЗДО, гораздо позже... */ @layer generic { body { background-color: tomato; } }
Цвет фона будет светло-зелёным, потому что нашему слою стилей компонентов задан больший приоритет, чем слою общих стилей. Таким образом, стили в слое компонентов «побеждают», даже если они написаны до слоя с общими стилями.
Опять же, это просто ещё один инструмент, чтобы контролировать, как CSS-каскад применяет стили. Инструмент, позволяющий более гибко организовывать что-то логически, а не бороться со специфичностью.
Теперь всё в ваших руках!
Главное здесь в том, что управляться с CSS-каскадом становится намного проще благодаря новым функциям. Мы видели, как псевдоселекторы :where
and :is
позволяют управлять специфичностью, исключив специфичность целого набора правил или взяв специфичность самого «тяжёлого» аргумента, соответственно. Затем мы использовали кастомные CSS-свойства для перекрытия стилей, не заводя еще один класс, чтобы перекрыть другой. Оттуда мы сделали небольшой крюк в сторону data-атрибутов, чтобы было проще создавать варианты компонентов, просто добавляя аргументы в HTML. И, напоследок, мы коснулись каскадных слоёв, которые наверняка покажут себя удобными для указания порядка загрузки стилей с помощью @layer
.
Я надеюсь, что вывод, который вы сделаете из этой статьи — это то, что CSS-каскад не так страшен, как его малюют. Мы получаем инструменты, чтобы перестать с ним бороться и начать еще активнее включать его в свой арсенал.
P.S. Это тоже может быть интересно: