Руководство по продвинутым CSS-селекторам. Часть 1

Перевод статьи «Guide to Advanced CSS Selectors — Part One».

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

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

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

Специфичность и каскад CSS

Чтобы успешно работать с селекторами в CSS, нужно разобраться в некоторых ключевых концепциях. Первая — это специфичность, а вторая — каскад (в «CSS» каскад, собственно, представлен буквой «C»).

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

Правильное использование каскада и специфичности селекторов позволит вам полностью избежать использования !important в ваших таблицах стилей.

Повышение специфичности происходит в результате отмены наследования из каскада.

Маленький пример. Какого цвета будет .item?

<div id="specific">
  <span class="item">Item</span>
</div>
#specific .item {
  color: red;
}

span.item {
  color: green;
}

.item {
  color: blue;
}

.item будет красным, потому что специфичность id-селектора выше, чем у каскада и селектора элемента.

Это не означает, что нужно добавлять #id ко всем вашим элементам и селекторам, но знать об их влиянии на специфичность нужно.

Ключевая идея: чем выше специфичность, тем сложнее перекрыть правило.

В каждом проекте свои нужды по части повторного использования правил. Желание делиться правилами с низкой специфичностью привело к подъему фреймворков, таких как Tailwind и Bulma.

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

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

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

Универсальный селектор

Photo by Dan Burton on Unsplash

Универсальный селектор — * — называется так потому, что применим ко всем элементам.

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

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

Применение универсального селектора на практике

Сброс блочной модели CSS

Чаще всего я использую универсальный селектор в самом первом правиле для сброса CSS:

*,
*::before,
*::after {
  box-sizing: border-box;
}

Этот код означает, что мы хотим, чтобы padding и границы во всех элементах входили в блочную модель, а не добавлялись к заданным размерам. Например, согласно следующему правилу, .box будет иметь ширину 200px, а не 200px + 20px padding-а.

.box {
  width: 200px;
  padding: 10px;
}
Вертикальный ритм

Еще один очень полезный вариант применения универсального селектора был предложен Энди Беллом и Хейдоном Пикерингом в их книге и на их сайте Every Layout. Этот способ называется «стек» и в самой простой форме выглядит так:

* + * {
  margin-top: 1.5rem;
}

При использовании со сбросом родительского правила, уменьшающего margin-ы всех элементов до нуля, это правило устанавливает верхний margin для каждого элемента, идущего за другим элементом. Это быстрый способ создать вертикальный ритм.

Если вы хотите быть более избирательным, в определенных обстоятельствах можно использовать этот селектор в качестве потомка:

article * + h2 {
  margin-top: 4rem;
}

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

Селекторы атрибутов

Это чрезвычайно мощная категория селекторов, но зачастую их мощь не используют в полной мере.

Знаете ли вы, что с помощью селекторов атрибутов можно добиться результатов сопоставления, как при применении регулярных выражений?

Это исключительно полезно при внесении изменений в системах в стиле БЭМ, где имена классов связаны между собой, но, возможно, нет какого-то одного общего имени класса.

Рассмотрим пример:

[class*="component_"]

Этот селектор выберет все элементы, имеющие класс, в имени которого содержится строка «component_». То есть, будут захвачены и component_title, и component_content.

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

[class*="component_" i]

Но указывать значение атрибута вообще не обязательно. Вы можете просто проверить, есть ли оно:

a[class]

Таким образом можно выбрать все ссылки a, имеющие любое значение класса.

Все возможные способы сопоставления со значениями в селекторах атрибутов можно найти в документации MDN.

Применение селекторов атрибутов на практике

Подчистка кода для улучшения доступности

Благодаря этим селекторам можно осуществлять базовый линтинг доступности, например:

img:not([alt]) {
  outline: 2px solid red;
}

При помощи этого кода мы добавим outline для всех изображений, не имеющих атрибута alt.

Применение к aria для принудительного обеспечения доступности

Если селектор атрибута используется как единственный селектор, с его помощью можно принудительно внедрить элементы доступности. В этом случае отсутствие атрибута предотвращает соответствующую стилизацию. Один из способов сделать это — добавить в нужное место атрибуты aria.

Например, при реализации аккордеона, где вам нужно включить следующую кнопку независимо от того, переключается ли логическое значение aria при помощи JavaScript:

<button aria-expanded="false">Toggle</button>

Теперь aria-expanded можно использовать в CSS в качестве селектора атрибута вместе с соседним родственным комбинатором для стилизации соответствующего контента как закрытого или открытого.

button[aria-expanded="false"] + .content { 
  /* hidden styles */
}
button[aria-expanded="true"] + .content { 
  /* visible styles */
}
Стилизация не-кнопочных ссылок навигации

При создании навигации вы можете иметь смесь дефолтных ссылок и ссылок, стилизованных как «кнопки». В этом случае для выбора не-кнопочных ссылок будет полезным использовать следующий код:

nav a:not([class])
Удаление стиля списка по умолчанию

Еще один совет от Энди Белла. Можно удалить стилизацию списка, основываясь на наличии атрибута role:

/* Remove list styles on ul, ol elements with a list role, 
which suggests default styling will be removed */
ul[role='list'],
ol[role='list'] {
  list-style: none;
}

Дочерний комбинатор

«Некоторые селекторы называются комбинаторами, поскольку они соединяют другие селекторы, создавая полезную связь селекторов друг с другом и расположением содержимого в документе», — MDN.

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

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

То есть:

  • article p захватывает все p внутри article,
  • article > p захватывает только абзацы, которые являются прямыми потомками article и не вложены в другие элементы.

Абзац захватывается article > p

<article>
  <p>Hello world</p>
</article>

Абзац не захватывается article > p

<article>
  <blockquote>
    <p>Hello world</p>
  </blockquote>
</article>

Применение дочернего комбинатора на практике

Многоуровневый список ссылок для навигации

Представьте себе навигацию в боковой панели в виде списка, например, на сайте документации, где есть вложенные уровни ссылок. Семантически это означает внешний ul, а также ul, вложенный в li.

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

nav > ul > li > a {
  font-weight: bold;
}

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

Определение области видимости для селекторов элементов

Мне нравится использовать селекторы элементов для основных частей моего макета страницы, таких как header или footer. Но это может вызвать проблемы, потому что у вас может быть, например, footer внутри blockquote или article.

В этом случае можно использовать не просто footer, а body > footer.

Стилизация встроенного (стороннего) контента

Порой у вас нет контроля над именами классов, идентификаторами и даже разметкой. Например, если речь идет о рекламе и прочем контенте, движимом JavaScript.

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

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

Общий родственный комбинатор

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

Например, p ~ img захватит все изображения, находящиеся где-нибудь после абзаца, если у них с абзацем общий родитель.

Это означает, что здесь изображения будут выбраны:

<article>
  <p>Paragraph</p>
  <h2>Headline 2</h2>
  <img src="img.png" alt="Image" />
  <h3>Headline 3</h3>
  <img src="img.png" alt="Image" />
</article>

А здесь не будут:

<article>
  <img src="img.png" alt="Image" />
  <p>Paragraph</p>
</article>

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

Применение общего родственного комбинатора на практике

Визуальная передача смены состояния

Если сочетать общий родственный комбинатор с селекторами псевдоклассов, такими как :checked, можно добиться интересных результатов.

Допустим, у нас есть чекбокс со следующим HTML:

<input id="terms" type="checkbox">
<label for="terms">I accept the terms</label>
<!-- series of <p> with the terms content -->

При помощи общего родственного комбинатора мы можем изменять стиль абзацев соглашения, только если чекбокс был отмечен:

#terms:checked ~ p {
  font-style: italic;
  color: #797979;
}
Вариации с низкой специфичностью

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

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

Это правило добавляет margin, уменьшает размер шрифта и осветляет цвет текста у любого элемента, который следует за изображением:

img ~ * {
  font-size: .9rem;
  color: #797979;
  margin: 1rem 1rem 0;
}

Можете поэкспериментировать с применением общего родственного комбинатора в CodePen.

Это правило имеет очень низкую специфичность, так что вы легко его перекроете добавлением какого-нибудь более направленного правила.

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

Чтобы лучше в этом разобраться, попробуйте завернуть контент элемента последней карточки в div. Цвет и margin будут наследоваться элементами div и type, но нативные стили браузера на h3 не позволят унаследовать размер шрифта от общего родственного комбинатора. Нативное правило браузера имеет более высокую специфичность, чем универсальный селектор, который технически нацелен на div.

Соседний родственный комбинатор

Соседний родственный комбинатор — + — захватывает элементы, идущие сразу за указанным элементом.

Мы уже пользовались этим в примерах с универсальным селектором * + * — чтобы применить верхний margin только к элементам, следующим за другим элементом. Давайте рассмотрим и другие примеры.

Применение соседнего родственного комбинатора на практике

Полифил для эмуляции gap в навигации

Поддержка gap во Flexbox скоро будет, но на момент написания статьи ее еще нет в Safari.

Поэтому, если у нас есть, например, навигация сайта, где очень нужен гибкий макет, мы можем добавить margin в качестве полифила для gap. Сделать это можно при помощи соседнего родственного комбинатора.

nav ul li + li {
  margin-left: 2rem;
}

Это сделает возможным gap-эффект между элементами списка без необходимости убирать дополнительный margin для первого:

Применение интервала между метками формы и входными данными

Рассматривая ранее «стек», мы говорили о применении margin только в одном направлении. Используя эту идею в рамках объектов полей формы, мы можем обеспечить достаточный интервал для сохранения визуальной иерархии между типами полей.

В этом случае мы добавляем намного меньший margin между меткой и ее входящими данными, и больший margin между входящими данными и метками:

label + input {
  margin-top: .25rem;
}

input + label {
  margin-top: 2rem;
}

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

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх