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


Недавно у меня появилось желание снова проверить себя и показать сообществу свои ответы на самые популярные вопросы по вёрстке. Может же быть так, что мои ответы были неполными или некорректными. Если это так, то я научусь новому. А если всё хорошо, то они помогут другим людям в подготовке к собеседованию. Кажется, это в любом случае будет полезно.


У меня получился достаточно большой список. Он составлен на основе моего опыта, опыта моих знакомых и публично доступных собеседований. Каждый вопрос будет отдельной статьёй.


Сегодня я дам ответ на следующий вопрос: «Какие знаешь псевдо-классы?».


:is() и :where()


В стандарте сказано, что псевдо-класс :is() — это функциональный псевдо-класса, который в качестве единственного аргумента принимает специальный список селекторов <forgiving-selector-list>.


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


Пользу псевдо-класса :is() лучше всего продемонстрировать сразу на примере. Допустим, мы ранее установили свойство margin-block для элементов <h1> и <h2>, находящихся в элементе с классом .awesome-block.


.awesome-block h1,
.awesome-block h2 {
  margin-block: 0;
}

Это старый стиль. Сегодня мы можем сократить код, объединив часть селекторов. Смотрите как.


.awesome-block :is(h1, h2) {
  margin-block: 0;
}

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


Группировать селекторы мы можем любым образом. Сначала, в середине или в конце. А можно даже объединить сразу в нескольких местах. В общем полная свобода.


:is(.awesome-block, .no-awesome-block) :is(h1, h2) {
  margin-block: 0;
}

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


Например, они применят свойство background-color со значением blue, не смотря на то, что я использую несуществующий селектор :some-unsupported-selector.


:is(.awesome-block, :some-unsupported-selector) {
  background-color: blue;
}

Единственной ложкой дёгтя является тот факт, что мы не можем использовать псевдо-элементы. В стандарте сказано так чёрным по белому. Я это узнал, когда хотел объединить псевдо-элементы ::before и ::after.


/* это код не работает! */
.awesome-block:is(::before, ::after) {
  background-color: blue;
}

Теперь мы можем перейти к псевдо-классу :where(). Я не просто так подробно рассказывал про псевд-класс :is(). Они очень сильно похожи! В стандарте прямо так и пишут. Сами посмотрите на следующий пример.


.awesome-block :where(h1, h2) {
  margin-block: 0;
}

Разница между ними заключается в принципе расчёта специфичности. Начнём с псевдо-класса :is().


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


Например, в следующем коде специфичность селектора будет 0100, потому что используется селектор идентификатор #unique-block. Поэтому применится значение red для свойства background-color.


:is(#unique-block, .awesome-block) {
  background-color: red;  /* выиграет это правило */
}

.awesome-block {
  background-color: blue;
}

У псевдо-класса :where() всё ещё проще. Не важно, какой селектор используется. Специфичность всегда будет 0000. По этой причине селектор по идентификатору проиграет селектору по классу в следующем примере.


.awesome-block {
  background-color: blue; /* выиграет это правило */
}

:where(#unique-block) {
  background-color: red;
}

Мне больше нечего рассказать про псевдо-классы :is() и :where(). Добавлю пару слов из своего опыта. Код действительно становится проще читать. Это, конечно, субъективная оценка, но, возможно, именно у вас будет также, как у меня.


Только поначалу можно запутаться со специфичностью. У меня так было. А потом всё пошло как по маслу. Сейчас проблем нет.


:has()


Согласно стандарту, псевдо-класс :has() — это функциональный псевдо-класс, который срабатывает для элемента, если хотя бы один из переданных селекторов применяется к связанному с ним элементу.


Звучит сложно? Согласен. Но такое определения дано в стандарте. Я предлагаю перейти к примерам. Так будет проще разобраться. Для объяснения я создал разметку.


<body>
  <div class="awesome-block">
    <span>1</span>
  </div>
  <div class="awesome-block">
    <span>2</span>
  </div>
  <div class="awesome-block">
    <span>3</span>
  </div>
  <div class="awesome-block">
    <span>4</span>
  </div>
</body>

В первом я написал правило к элементу с классом .awesome-block, если у него есть соседний элемент с таким же классом.


.awesome-block:has(+ .awesome-block) {
  background-color: lightblue;
}

Четыре квадрата отображены в строку. У первого, второго и третьего квадрата цвет светло-голубой

А в этом примере правило применится, если у элемента есть элемент <span> среди потомков.


.awesome-block:has(span) {
  background-color: lightblue;
}

Четыре квадрата отображены в строку. У всех светло-голубой цвет

Получается псевдо-класс :has() сработает, когда у элемента есть «связь» с другим элементом в объектной модели документа. Это могут быть соседние элементы или дочерние.


В первом примере псевдо-класс `:has()` сработал для всех элементов, у которых есть соседний элемент с классом .awesome-block. Во втором примере он сработал для всех элементов с классом .awesome-block, потому что у них есть дочерний элемент <span>.


В качестве аргумента мы можем передать несколько селекторов. Например, следующее правило сработает, если у элемента с классом .awesome-block есть среди потомков элемент <h2> или <h3>.


.awesome-block:has(h2, h3) {
  background-color: lightblue;
}

Этот пример также хорошо иллюстрирует факт, что у псевдо-класса :has() встроена логика. По сути мы говорим браузерам: «Найдите нам или элемент <h2> или элемент <h3>». То есть браузеры работают по логике оператора ИЛИ.


А отдельная классная штука заключается в том, что мы можем изменить эту логику! Если мы напишем псевдо-классы :has() друг за другом, то браузеры будут работать по логике оператора И.


.awesome-block:has(h2):has(h3) {
  background-color: lightblue;
}

В этом случае правило применится, только если в элементе с классом .awesome-block есть элементы <h2> и <h3>. Если есть только один из них, то браузеры не применят правило.


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


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


/* эти два правила не сработают! */
.awesome-block:has(h1, h2:has(span)) {
  font-size: 3rem;
}

.awesome-block:has(h1, h2::before) {
  font-size: 3rem;
} 

Отдельное внимание обращу на то, что в случае с псевдо-классом :has() правило полностью игнорируется, даже если передан корректный селектор. В моём примере это селектор h1. Так что нам требуется запомнить эти ограничения.


Начиная разговор о специфичности, стоит сказать, что она напоминает случай с псевдо-классом :is(). Рассчитывается по наиболее весомому селектору среди переданных. Например, в следующем примере специфичность псевдо-класса :has() будет 0001, а всего правила — 0011, потому что используется селектор по классу.


.awesome-block:has(h1) {
  background-color: lightblue;
}

Больше ничего не знаю про псевдо-класс :has(). Я просто хочу очень сильно порекомендовать обратить на него внимание. Он очень полезен при реализации изменяемых компонентов. Когда добавляются или удаляются элементы, происходит изменения состояний или любое динамическое изменение документа.


:nth-child()


Псевдо-класс :nth-child() — это псевдо-класс, выбирающий соседние элементы на основании нотации An+B [of S]. Всего существует два способа работы. Рассмотрим каждый.


Начнём мы с части An+B. Да, если честно, она тоже слишком сложна. Давайте упростим. Оставим просто символ n и будем его использовать для псевдо-класса :nth-child().


.awesome-block:nth-child(n) {
  background-color: lightblue; /* найдутся элементы №1, №2, №3, и т. д. */
}

При таком синтаксисе правило применяется ко всем элементам, находящимся на одном уровне в объектной модели документа. Как так получилось? Всё дело в символе n.


n— это целое число. Браузеры всегда динамически увеличивают его на единицу, начиная с 0. На каждое увеличение происходит вычисление формулы An+B. Результат используется для поиска элементов.


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


.awesome-block:nth-child(n+2) {
  background-color: lightblue; /* найдутся элементы №2, №3, №4 и т.д. */
}

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


.awesome-block:nth-child(2n+2) {
  background-color: lightblue; /* найдутся элементы №2, №4, №6 и т.д. */
}

Мы можем упростить работу браузерам, указав конкретную позицию элемента. Например, вторую.


.awesome-block:nth-child(2) {
  background-color: lightblue;
}

Важно понимать, что при использовании формулы An+B поиск элементов привязан только к их позиции в объектной модели документа. Это может привести к путанице.


Для демонстрации давайте найдём второй элемент с классом .awesome-block.


<body>
  <span class="awesome-block">1</span>
  <span>2</span>
  <span class="awesome-block">3</span>
</body>

.awesome-block:nth-child(2) {
  background-color: lightblue;
}

Три квадрата в строке. У всех томатный цвет


Не получилось. Можно подумать, что это странное поведение. Ведь у нас есть второй элемент с классом .awesome-block. Только с точки зрения браузеров у него уже не вторая, а третья позиция в объектной модели документа. А у элемента со второй позицией класса .awesome-block нет. Поэтому они не смогли применить стили.


Мы можем сказать браузерам, чтобы они при поиске элемента учитывали ещё селекторы, в частности класс. Для этого нужно использовать второй синтаксис псевдо-класса :nth-child(), а именно An+B of S нам поможет. Перепишем предыдущее правило.


:nth-child(2 of .awesome-block) {
  background-color: lightblue;
}

Три квадрата в строке. У первого и второго томатный цвет, а у третье светло-голубой


Работает! Давайте разбираться, как же в этом случае работает псевдо-класс :nth-child().


В этом варианте синтаксиса мы сделали несколько действий. Во-первых, опустили символы A и B и задали 2 в качестве значения для символа n. Получается, браузеры будут искать вторую позицию. Так, а где? Для ответа на этот вопрос рассмотрим, что означает символ S.


Указав селекторы после of в круглых скобках, мы формируем список разрешённых селекторов. В стандарте он называется <complex-selector-list>. Браузеры с помощью него находят все нужные элементы.


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


В нашем примере с помощью селектора .awesome-block сформировался список из двух элементов, а потом правило применилось ко второму элементу.


Давайте рассмотрим ещё один пример. Мы добавим ещё один селектор по классу в псевдо-классе :nth-child().


<body>
  <span class="awesome-block">1</span>
  <span>2</span>
  <span class="secondary-awesome-block">3</span>
  <span class="awesome-block">4</span>
  <span class="secondary-awesome-block">5</span>
</body>

:nth-child(2 of .awesome-block, .secondary-awesome-block) {
  background-color: lightblue;
}

Когда я только столкнулся с синтаксисом of S я думал, что можно сначала применить стили ко второму элементу с классом .awesome-block, а потом ещё ко второму элементу с классом .secondary-awesome-block. Нужно просто перечислить классы через запятую, как я сделал в примере. Конечно, я думал неправильно.


В синтаксисе of S важно помнить, что используется список разрешённых селекторов, по которому формируется список элементов. В нём происходит поиск. По этой причине в моём примере браузеры с помощью классов .awesome-block и .secondary-awesome-block нашли по два элемента и добавили их в список. В нём получилось четыре элемента. Среди них они нашли второй элемент и к нему применили правило. А это был первый элемент с классом .secondary-awesome-block.


Пять квадратов в строке. У третьего светло-голубой цвет


В обоих примерах я использовал просто символ n, опустив символы A и B. Но их тоже можно использовать. Работать они будут также, как я показывал раньше.


:nth-child(2n+2 of .awesome-block, .secondary-awesome-block) {
  background-color: lightblue;
}

Пять квадратов в строке. У третьего и пятого светло-голубой цвет


В этом примере браузеры нашли все элементы с классами .awesome-block и .secondary-awesome-block. Среди них они применяют правило через два элемента, начиная со второго.


Осталось рассмотреть расчёт специфичности. У псевдо-класса :nth-child() она равняется 0010. Если мы используем синтаксис of S, то к базовой прибавляется специфичность самого весомого селектора в списке разрешённых.


Например, я использую селектор по классу, поэтому к базовой специфичности 0010 прибавляется ещё специфичность по классу. В итоге специфичность всего правила будет 0020.


:nth-child(2n+2 of .awesome-block) {
  background-color: lightblue;
}

Подводя итог разделу, скажу своё личное мнение по псевдо-классу :nth-child(). Он постоянно попадается в практике. Особенно в проектах с легаси кодом. Но я лично стараюсь минимизировать его использование в пользу более простых решений.


▍ Заключение


Отвечу кратко на вопрос: «Какие знаешь псевдо-классы?».


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


В своей работе я часто использую следующие псевдо-классы: :is(), :where(),:has(), :nth-child(), :hover и :focus-visible.


На этом сегодня всё. Другие статьи из серии можно найти по тегу «sm909_css_interview».


Пожалуйста, напишите свои вопросы, которые сбили вас с толку на собеседовании. Я попробую ответить на них в новой статье. Спасибо за чтение!


P.S. Помогаю больше узнать про CSS в своём ТГ-канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле.


© 2024 ООО «МТ ФИНАНС»


Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (1)


  1. vanxant
    11.12.2024 19:48

    :has удобно вешать на body, чтобы реагировать на всякие потусторонние фигни, на которые нельзя ни повлиять, ни отказаться. Всякие там CRM-кнопки, свистоперделки маркетолухов и т.д., которые при всплытии например ломают нам макет страницы или отдельные элементы. Тогда можно что-то починить через body:has(.crm-popup.shown) .myitem { ... }