Три подхода к селектору & (амперсанд) в CSS

Источник: «Three Approaches to the & (ampersand) Selector in CSS»
& — мощное дополнение к CSS, позволяющее создавать селекторы без повторений и способствующее улучшению организованности и понимания кода.

В CSS селектор & (символ амперсанда) добавляет правила стиля, основанные на соотношении между вложенными селекторами. Например, псевдокласс (:hover), вложенный в селектор типа (div), становится составным селектором (div:hover), если вложенному псевдоклассу присвоен префикс &.

div {
&:hover {
background: green;
}
}

/*
Приведённый выше код эквивалентен:
div:hover {
background: green;
}
*/

Селектор & можно использовать в сочетании с псевдоклассом :has() для выбора и стилизации элементов на основе дочерних элементов, которые они содержат. В следующем примере label стилизуется под отмеченный внутри него чекбокс.

<label>
<input type="checkbox">
Allow access to all files
</label>
label {
/* и т.д. */
&:has(:checked) {
border: 1px solid lime;
}
}

/*
Приведённый выше код эквивалентен:

label:has(:checked) {
border: 1px solid lime;
}
*/

See the Pen

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

Связанные имена классов

Начнём с самого простого подхода: селектор & можно использовать для объединения имён классов. Элементы часто содержат несколько имён классов для их группировки и стилизации. Иногда группировка происходит внутри модуля, а иногда правила стиля могут пересекаться между модулями из-за общих имён классов.

В приведённом ниже примере три модуля карточек, объединённых в класс cards, имеют общие правила стиля, такие как размеры. Однако у них разные фоновые изображения, отражающие их содержание, определяемое отдельно в наборе правил .cards путём конкатенации селектора & с эксклюзивными именами классов каждой карточки.

<div class="cards trek">
<p>Trekking</p>
</div>
<div class="cards wildlife">
<p>Wildlife spotting</p>
</div>
<div class="cards stargaze">
<p>Stargazing camp</p>
</div>
.cards {
background: center/cover var(--bg);

&.trek {
--bg: url("trek.jpg");
}
&.wildlife {
--bg: url("wildlife.jpg");
}
&.stargaze {
--bg: url("stargaze.jpg");
}
}

See the Pen

Имена классов также можно подключать с помощью селектора атрибутов:

<div class="element-one">text one</div>
<div class="element-two">text two</div>
[class|="element"] {
&[class$="one"] { color: green; }
&[class$="two"] { color: blue; }
}

Ещё один пример:

<div class="element_one">text one</div>
<div class="element_two">text two</div>
[class^="element"] {
&[class$="one"] { color: green; }
&[class$="two"] { color: blue; }
}

Селекторы родительского и предыдущего элементов

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

<p class="error">
Server down. Try again after thirty minutes.
If you still can't access the site,
<a href="/errorReport">submit us a report</a>.
We'll resolve the issue within 24hrs.
</p>
.error {
color: red;
a {
color: navy;
}
}

Однако благодаря селектору & возможно и обратное: вложение правил стиля родительского элемента в набор правил его дочернего элемента. Это может быть удобно, когда проще организовать стилевые правила элемента по его назначению, а не по его положению в макете.

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

В следующем примере все правила, связанные с внешним видом модулей соглашения при загрузке страницы, такие как макет, размеры и цвета, включены в набор правил .agreements. Однако правила стиля, изменяющие внешний вид модулей соглашения при установке чекбоксов, т. е. при взаимодействии с пользователем, помещаются во вложенный селектор .isAgreed:checked.

<article class="agreements terms">
<header><!-- ... --></header>
<section>
<input class="isAgreed" type="checkbox" />
<!-- ... -->
</section>
<footer><! -- ... --></footer>
</article>
<article class="agreements privacy">
<header><!-- ... --></header>
<section>
<input class="isAgreed" type="checkbox" />
<!-- ... -->
</section>
<footer><! -- ... --></footer>
</article>
<article class="agreements diagnostics">
<header><!-- ... --></header>
<section>
<input class="isAgreed" type="checkbox" />
<!-- ... -->
</section>
<footer><!-- ... --></footer>
</article>
.agreements {
/* и т.д. */
&.terms { --color: rgb(45 94 227); }
&.privacy { --color: rgb(231 51 35); }
&.diagnostics { --color: rgb(59 135 71); }
/* и т.д. */
}

.isAgreed:checked {
/* изменение цвета фона чекбокса */
background: var(--color);
/* изменение цвета границы чекбокса */
border-color: var(--color);
/* чекбокс отображает отметку */
&::before { content: '\2713'; /* отметка (✓) */ }

/* Изменение цвета границы раздела соглашения (родителя чекбокса) */
.agreements:has(&) {
/* то же самое, что и .agreements:has(.isAgreed:checked)*/
border-color: var(--color);
}
}

See the Pen

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

<p>some text</p>
<input type="checkbox"/>
/* Когда чекбокс отмечен */
:checked {
/*
правила стилизации чекбокса
*/


/* стиль для <p>, когда чекбокс отмечен */
p:has(+&) {
/* то же, что и p:has(+:checked) */
color: blue;
}
}

Рекуррентные селекторы

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

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

<article>
<header><!--...--></header>

<p><!--...--></p>

<figure><!--...--></figure>

<p><!--...--></p>
<p><!--...--></p>
<p><!--...--></p>

<blockquote><!--...--></blockquote>

<p><!--...--></p>
<p><!--...--></p>

<figure><!--...--></figure>

<p><!--...--></p>

<footer><!--...--></footer>
</article>
article {
/* и т.д. */
p {
margin: 0;

/* <p> находящийся после/ниже элемента, не являющегося <p> */
*:not(&) + & {
margin-top: 30px;
}

/* <p> находящийся перед/над элементом, не являющимся <p>. */
&:not(:has(+&)) {
margin-bottom: 30px;
}

/* <p> что расположенное после/под другим <p> */
& + & {
margin-top: 12px;
}
}
/* и т.д. */
}

See the Pen

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

Объяснить работу селекторов можно следующим образом:

Помимо гибкости, ещё одна веская причина использования селектора & при организации кода заключается в том, что он не имеет собственной специфичности. Это означает, что можно положиться на специфичность обычных селекторов и иерархию вложенности, чтобы применить правила по назначению.

Использование & в ванильном CSS vs. использование & в фреймворках

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

<div class="parent--one">text one</div>
<div class="parent--two">text two</div>

В Sass (SCSS) стиль может быть следующим:

.parent {
&--one { color: green; }
&--two { color: blue; }
}

Это не будет работать в ванильном CSS, но это всё равно можно сделать! Аналогичный набор правил будет выглядеть так:

[class|="parent"] {
&[class$="--one"] { color: green; }
&[class$="--two"] { color: blue; }
}

Обратите внимание, что в коде SCSS нет реального селектора .parent, так как ему не соответствует ни один элемент на странице, в то время как в CSS [class|="parent"] будет соответствовать элементу. Если добавить правило стиля в набор правил внешнего уровня в обоих этих фрагментах кода, SCSS не сможет найти элемент для применения правила стиля, в то время как CSS применит стиль к обоим элементам, имя класса которых начинается с parent.

.parent {
font-weight: bold; /* это не сработает, так как не соответствует ничему */
&--one { color: green; }
&--two { color: blue; }
}
[class|="parent"] {
font-weight: bold; /* работает */
&[class$="one"] { color: green; }
&[class$="two"] { color: blue; }
}

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

При использовании вложенного правила стиля необходимо иметь возможность ссылаться на элементы, которым соответствует родительское правило; в этом, в конце концов, и заключается смысл вложенности. Для этого в данной спецификации определён новый селектор — селектор вложенности, записываемый как & (U+0026 AMPERSAND).

При использовании в селекторе вложенного правила стиля, селектор вложенности представляет элементы, соответствующие родительскому правилу. При использовании в любом другом контексте он представляет те же элементы, что и :scope в этом контексте (если не определено иное).

CSS Nesting Module 1, W3C

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

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

Комментарии


Дополнительные материалы

Предыдущая Статья

Ускорение сборки Docker с помощью кэша сборки

Следующая Статья

Простые тесты конечных точек с Policy::fake