Skip to main content

CSS псевдокласи та псевдоелементи

CSS псевдокласи та псевдоелементи - це два типи селекторів з однією ключовою різницею: псевдокласи вибирають цілі елементи за станом або позицією, а псевдоелементи стилізують конкретні частини елементів або вставляють контент, якого немає в DOM.

Теорія

TL;DR

  • Псевдокласи використовують одну двокрапку (:hover); псевдоелементи - подвійну (::before)
  • Псевдокласи відповідають реальним вузлам DOM за станом або позицією; псевдоелементи створюють або вибирають віртуальні підчастини
  • Уяви псевдокласи як індикатор стану елемента (реагує на те, що відбувається); псевдоелементи як стікер, приклеєний до конкретного місця
  • Взаємодія з користувачем і позиція в DOM - псевдоклас. Декорація або вставка контенту - псевдоелемент.
  • ::before і ::after потребують властивості content, навіть якщо вона порожня

Швидкий приклад

css
/* Псевдоклас: вибирає за позицією */ li:nth-child(odd) { background: #f0f0f0; } /* Псевдоелемент: вставляє контент перед кожним li */ li::before { content: "→ "; }
html
<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-змінну як міст між скриптом і псевдоелементом.

Типові помилки

Одна двокрапка для псевдоелементів:

css
/* Стара нотація - уникай */ p:before { content: ""; } /* Правильно */ p::before { content: ""; }

Одна двокрапка досі працює в більшості браузерів для зворотної сумісності, але змішує синтаксис псевдокласів і псевдоелементів.

:nth-child рахує не тих сусідів:

css
.item:nth-child(2) { color: red; }
html
<div><span class="item">1</span></div> <div class="item">2</div> <!-- НЕ червоний: він 1-й нащадок свого батька -->

:nth-child рахує серед сусідів одного батька, а не серед усіх .item у DOM. Якщо елементи знаходяться в різних контейнерах, лічильник скидається для кожного.

Відсутній content у ::before або ::after:

css
/* Нічого не рендериться - content обов'язковий */ .icon::before { font-family: "Icons"; } /* Правильно */ .icon::before { content: ""; font-family: "Icons"; }

Прибирання :focus outline без заміни:

css
/* Ламає навігацію з клавіатури */ 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. На практиці різниця мала, але при тисячах елементів реальні вузли можуть виявитися швидшими.

Приклади

Зебра-смугування зі статусними іконками

css
.issue { padding: 1rem; border-bottom: 1px solid #eee; } .issue:nth-child(odd) { background: #f6f8fa; } .issue.severity-high::before { content: "🔥 "; font-size: 1.2em; }
html
<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 у вкладених списках

css
/* Неправильно: рахує глобально, ламається при кількох списках */ .row:nth-child(2n) { background: #f0f0f0; } /* Правильно: обмежено батьківським контейнером */ .list .row:nth-child(2n) { background: #f0f0f0; }
html
<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-компонентах, які рендеряться в окремі контейнери.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?