CSS псевдокласи та псевдоелементи
CSS псевдокласи та псевдоелементи - це два типи селекторів з однією ключовою різницею: псевдокласи вибирають цілі елементи за станом або позицією, а псевдоелементи стилізують конкретні частини елементів або вставляють контент, якого немає в DOM.
Теорія
TL;DR
- Псевдокласи використовують одну двокрапку (
:hover); псевдоелементи - подвійну (::before) - Псевдокласи відповідають реальним вузлам DOM за станом або позицією; псевдоелементи створюють або вибирають віртуальні підчастини
- Уяви псевдокласи як індикатор стану елемента (реагує на те, що відбувається); псевдоелементи як стікер, приклеєний до конкретного місця
- Взаємодія з користувачем і позиція в DOM - псевдоклас. Декорація або вставка контенту - псевдоелемент.
::beforeі::afterпотребують властивостіcontent, навіть якщо вона порожня
Швидкий приклад
/* Псевдоклас: вибирає за позицією */
li:nth-child(odd) { background: #f0f0f0; }
/* Псевдоелемент: вставляє контент перед кожним li */
li::before { content: "→ "; }<ul>
<li>Item 1</li> <!-- сірий фон + стрілка -->
<li>Item 2</li> <!-- немає фону + стрілка -->
<li>Item 3</li> <!-- сірий фон + стрілка -->
</ul>Псевдоклас змінює вигляд залежно від позиції в DOM. Псевдоелемент додає віртуальний контент, якого немає в HTML-розмітці.
Головна різниця
Псевдокласи реагують на умови зовні елемента: дію користувача, позицію в DOM або стан (:checked, :disabled). Псевдоелементи натомість створюють анонімні блоки в дереві рендерингу браузера. Ці блоки є в движку верстки, але не мають відповідного вузла DOM. Саме тому ::before не можна знайти через document.querySelector - в DOM просто нічого шукати.
Коли що використовувати
- Стани hover і focus на кнопках і посиланнях:
:hover,:focus - Зебра-смугування в списках або таблицях:
:nth-child(odd),:nth-child(2n) - Стилізація форм за станом:
:checked,:disabled,:required - Декоративні іконки або лічильники до або після елементів:
::before,::after - Буквиці або стилізований перший рядок у статтях:
::first-letter,::first-line - Колір виділення тексту:
::selection
Якщо додаєш порожній <span> тільки заради декорації - це сигнал, що ::before або ::after впораються без зайвої розмітки.
Таблиця порівняння
| Аспект | Псевдокласи | Псевдоелементи |
|---|---|---|
| Синтаксис | Одна двокрапка :hover | Подвійна двокрапка ::before |
| Що вибирає | Цілі існуючі елементи | Частини або віртуальні піделементи |
| Наявність у DOM | Відповідає реальним вузлам | Генерує блоки тільки для рендерингу |
| Поширені приклади | :hover, :nth-child(2n), :not(), :has() | ::before, ::after, ::first-line, ::selection |
| Доступний з JS | Так | Ні |
| Коли використовувати | Стилізація за станом або позицією | Декоративний контент, стилізація частин елемента |
Як браузер це обробляє
Коли движок стилів розбирає селектори, псевдокласи перевіряють динамічні умови через дерево обчислених стилів. :hover спрацьовує при події mouseenter на елементі. Псевдоелементи, наприклад ::before, генерують анонімні рядкові блоки в дереві верстки. Ці блоки успадковують стилі від батька, але не мають реального HTML-вузла. Змінити вміст ::before через JS напряму не вийде - для цього зазвичай використовують CSS-змінну як міст між скриптом і псевдоелементом.
Типові помилки
Одна двокрапка для псевдоелементів:
/* Стара нотація - уникай */
p:before { content: ""; }
/* Правильно */
p::before { content: ""; }Одна двокрапка досі працює в більшості браузерів для зворотної сумісності, але змішує синтаксис псевдокласів і псевдоелементів.
:nth-child рахує не тих сусідів:
.item:nth-child(2) { color: red; }<div><span class="item">1</span></div>
<div class="item">2</div> <!-- НЕ червоний: він 1-й нащадок свого батька -->:nth-child рахує серед сусідів одного батька, а не серед усіх .item у DOM. Якщо елементи знаходяться в різних контейнерах, лічильник скидається для кожного.
Відсутній content у ::before або ::after:
/* Нічого не рендериться - content обов'язковий */
.icon::before { font-family: "Icons"; }
/* Правильно */
.icon::before { content: ""; font-family: "Icons"; }Прибирання :focus outline без заміни:
/* Ламає навігацію з клавіатури */
button:focus { outline: none; }Прибирай outline тільки якщо замінюєш його чимось не менш помітним. WCAG 2.1 AA вимагає видимих індикаторів фокусу. Більшість багів із доступністю в продакшені, які я бачив, починаються саме з цього рядка.
Де зустрічається в реальних проєктах
- Tailwind CSS: варіанти
hover:,focus:компілюються в псевдокласи:hover,:focus - Bootstrap 5:
::before/::afterдля іконок шевронів в акордеонах і дропдаунах - GitHub:
:nth-child(odd)для зебра-смугування списку PR,::beforeдля декорацій лейблів - Material-UI:
:checkedдля станів перемикачів,::selectionдля брендованого виділення тексту
Питання на співбесіді
Q: Яка різниця між :hover у CSS і mouseenter у JavaScript?
A: :hover розповсюджується на дочірні елементи, тому при наведенні на вкладений елемент батько теж вважається hovered. mouseenter в JS спрацьовує тільки на прямому цільовому елементі без бульбашкового розповсюдження.
Q: Чи можна вкладати псевдоелементи, наприклад ::before::after?
A: Ні. Специфікація не дозволяє псевдоелементам мати власні псевдоелементи. Замість цього використовуй реальний дочірній елемент.
Q: Яка специфічність у :not(.foo)?
A: Сам :not() не додає специфічності. Її визначає тільки аргумент всередині. Тому :not(.foo) має специфічність на рівні класу (0,1,0), як і просто .foo.
Q: Як :has() змінює підхід до псевдокласів?
A: :has() - це батьківський селектор (CSS Selectors 4), підтримується в Chrome 105+ і Safari 15.4+. section:has(.warning) { border: 2px solid red; } робить те, для чого раніше потрібен був JavaScript.
Q (рівень senior): Яка різниця у продуктивності рендерингу між 100 елементами ::before і 100 додатковими <span>?
A: Псевдоелементи створюють додаткові блоки в дереві верстки, що збільшує час paint і layout. Реальні вузли DOM теж мають вартість, але браузери для них краще оптимізовані. Для великих списків з ::before - профілюй через панель Layout у Chrome DevTools. На практиці різниця мала, але при тисячах елементів реальні вузли можуть виявитися швидшими.
Приклади
Зебра-смугування зі статусними іконками
.issue { padding: 1rem; border-bottom: 1px solid #eee; }
.issue:nth-child(odd) { background: #f6f8fa; }
.issue.severity-high::before {
content: "🔥 ";
font-size: 1.2em;
}<div class="issue severity-high">Fix login bug</div> <!-- сірий + іконка вогню -->
<div class="issue">Update docs</div> <!-- білий, без іконки -->
<div class="issue severity-high">Security patch</div> <!-- сірий + іконка вогню -->:nth-child(odd) смугує рядки за позицією. ::before на конкретному класі додає декоративний контент тільки для пріоритетних елементів. Жодних зайвих HTML-тегів для жодного з ефектів.
Обмежений :nth-child у вкладених списках
/* Неправильно: рахує глобально, ламається при кількох списках */
.row:nth-child(2n) { background: #f0f0f0; }
/* Правильно: обмежено батьківським контейнером */
.list .row:nth-child(2n) { background: #f0f0f0; }<div class="list">
<div class="row">A</div> <!-- немає фону -->
<div class="row">B</div> <!-- сірий -->
</div>
<div class="list">
<div class="row">C</div> <!-- сірий з правильною версією -->
</div>Без прив'язки до .list :nth-child рахує всіх сусідів .row по всій сторінці. Додай батьківський селектор - і кожен список отримає власний незалежний лічильник. Це часта причина помилок зі смугуванням у React-компонентах, які рендеряться в окремі контейнери.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.