Не боритесь с каскадом, управляйте им!
Перевод статьи 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. Это тоже может быть интересно:
Это не применимо к реальной разработке под типового пользователя. Многое из описанного появилось лишь в 20 или 21 году, не считая поддержку в IE (не Edge), но это не главное.
Главное, что это велосипед, причем, такой, мощный. Дело в том, что каскады работают из без слоев, и эти :is и :where не более чем костыли, которые позволяют управлять приоритетом там, где это должно решать обычными средствами.
Одна из идей приоритетов каскадов CSS в том, чтобы кастомизировать отображение элементов в более специфичных условиях, т.е. для случаев, когда в конкретном случае надо менять стиль элемента, надо использовать обычные каскады.
Вероятно, дело в том, что некоторые разработчики исходно пишут через каскады, но это большой минус и именно поэтому многие ушли к нотации по БЭМ или к модульному CSS – потому что это адски тяжело для браузера (в разработке под мобильные девайсы это вообще табу, использовать серьезные каскады), а вопросы отображения легко решаются без вложенных селекторов.
Типовое решение если нужно доопределить отображение через CSS – добавьте, внимание, еще один класс. Классы можно приоритезировать даже если нет каскадности, просто их порядком. По этому принципу работают модификаторы без каскадности (по БЭМ).
Поэтому всё вот это выше, это интересно, модно, но это не реальное применение. Если примените это на практике в большом проекте – получите сложность поддержки (вместо простых классов получите каскады, переменные, кучу селекторов, и прочее), проблемы с производительностью, и, возможно, порежете аудиторию, которую может поддерживать ваше приложение.
За статью 4+, хорошо оформлена
Нельзя :) Порядок селекторов в CSS-коде — часть каскада.
Мне кажется за такой дикий каскад в реальной разработке голову оторвут. потому-что «Как это можно поддерживать»?