Главная » Css live » Не боритесь с каскадом, управляйте им!

Не боритесь с каскадом, управляйте им!

Перевод статьи 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 — цвет фона, а -bdclborder-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. Это тоже может быть интересно: