Не боритесь с CSS каскадом. Контролируйте его!

  alexei 31/01/2022 - 09:54
Не боритесь с CSS каскадом. Контролируйте его!

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

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

Структурные слои CSS
Структурные слои CSS

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

Однако, даже с этими двумя подходами, все еще бывают моменты, когда мы продолжаем бороться с каскадом. Например, наверняка вам приходилось импортировать компоненты CSS при помощи директивы @import в определенном месте, или же использовать ужасную декларацию !important.

Недавно в инструментарий CSS были добавлены некоторые новые инструменты, которые позволяют, наконец, управлять каскадом. Давайте посмотрим на них.

О каскад, где ты?

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

Для начала представьте себе некоторые общие стили элемента <table>, использующие :where:


:where(table) {
  background-color: tan;
}

Теперь, если вы добавите некоторые другие стили таблиц перед селектором :where, например:


table {
  background-color: hotpink;
}

:where(table) {
  background-color: tan;
}

...фон таблицы станет hotpink, даже если селектор table стоит перед селектором :where в каскаде. В этом вся прелесть псевдоселектора :where, и именно поэтому он уже используется для сброса CSS.

У :where есть родной брат, который имеет почти прямо противоположный эффект - псевдоселектор :is.

Согласно спецификации Селекторов Уровень 4:

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

Вернемся в нашему предыдущему примеру и расширим его:


:is(table) {
  --tbl-bgc: orange;
}
table {
  --tbl-bgc: tan;
}
:where(table) {
  --tbl-bgc: hotpink;
  background-color: var(--tbl-bgc);
}

Цвет фона у элемента <table class="c-tbl"> будет tan, потому что специфичность :is такая же, как у table, но table стоит после него.

Однако, если мы изменим этот код так:


:is(table, .c-tbl) {
  --tbl-bgc: orange;
}

...цвет фона станет orange, так как у :is вес самого тяжелого селектора, которым и является .c-tbl.

Пример: Настраиваемый компонент таблицы

Теперь давайте посмотрим, как мы можем использовать псевдоселектор :where в наших компонентах. Мы будем создавать компонент таблицы, начиная с HTML:

Давайте завернем .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%);
  }
}

Теперь мы можем создавать вариации нашего компонента таблицы, вводя другие стили до или после наших общих стилей (благодаря возможностям удаления специфичности псевдоселектором :where), либо перезаписывая элемент .c-tbl, либо добавляя класс-модификатор в стиле BEM (например, 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%);
}

Отлично! Но обратите внимание, что мы продолжаем повторять цвета. А что, если мы захотим изменить border-radius или border-width? Это привело бы к большому количеству повторяющегося кода CSS.

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

Пользовательские свойства CSS

Вернемся к HTML коду компонента и в элемент таблицы добавим атрибут data-component, который может использоваться для стилизации.


<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-hue в цвете HSL на другое, установить --tbl-bdrs: 0 для удаления border-radius и так далее.

Стили таблицы в Инструментах разработчика
Набор правил CSS с псевдоселектором :where, показывающий пользовательские свойства таблицы

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

Мы также можем использовать пользовательские свойства, чтобы контролировать выравнивание и ширину столбцов:


:where[data-component="table"] tr > *:nth-of-type(1)) {
  text-align: var(--ca1, initial);
  width: var(--cw1, initial);
  /* повторите для столбцов 2 и 3... */
}

В панели Инструменты разработчика выберите таблицу и добавьте стили в селектор element.styles:


element.style {
  --ca2: center; /* Выравнивание 2 столбца по центру */
  --ca3: right; /* Выравнивание 3 столбца по правому краю */
}

Выравнивание столбцов таблицы
Выравнивание столбцов таблицы

Теперь давайте создадим наши особые стили компонентов, используя обычный класс .c-tbl (который на BEM означает "компонент таблица"). Давайте добавим этот класс в разметку таблицы:


<table class="c-tbl" data-component="table" id="table">

Теперь, прежде чем мы начнем возиться со всеми значениями свойств, давайте изменим CSS значение --tbl-hue, чтобы посмотреть, как это работает:


.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
Светлая таблица с параметром noinlineborder... о котором будет рассказано ниже

Добавление параметров с другим атрибутом data-

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

  • цветовое чередование строк и столбцов,
  • прилипающий заголовок и столбец,
  • опции состояния наведения курсора, такие как, наведение на ячейку, строку, столбец.

Мы могли бы просто добавить модифицирующие классы в стиле BEM, но на самом деле мы можем сделать это более эффективно, добавив в набор еще один атрибут 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;
}

Вот демонстрационная версия, которая позволяет вам изменять один параметр за раз:

Светлая тема демонстрационной таблицы имеет такие значения:


.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- является единственно верным, было бы чересчур самоуверенным. Именно такой способ выбрали мы, но вы можете положиться на любой другой метод, с которым вам удобно работать, будь то класс-модификатор BEM или что-то еще.

Суть в следующем: возьмите в свой арсенал псевдоселекторы :where и :is и их возможности управлять каскадом. И, если возможно, конструируйте CSS таким образом, чтобы при создании новых вариантов компонентов вам приходилось бы писали как можно меньше нового кода CSS!

Теперь у вас все под контролем!

Благодаря новым функциям управлять каскадом CSS становится намного проще. Мы увидели, как псевдоселекторы :where и :is позволяют контролировать специфичность, либо устраняя ее из всего набора правил в первом случае, либо принимая специфичность какого-то конкретного аргумента во втором. Затем мы использовали пользовательские свойства CSS для переопределения стилей без написания переопределяющего нового класса. Оттуда мы сделали небольшой крюк в сторону атрибутов data-, добавив больше гибкости при создании новых вариантов компонентов.

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