Skip to main content

CSS селектори

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

Теорія

Коротко

  • Селектор працює як фільтр по DOM. p знаходить усі абзаци. p.note знаходить тільки абзаци з класом note. nav > ul > li знаходить тільки прямі li всередині прямого ul всередині nav.
  • Базові селектори (тег, клас, ID) знаходять елементи за тим, що вони є. Комбінатори знаходять за тим, де вони стоять у дереві.
  • Специфічність (specificity): inline = 1000, ID = 100, клас = 10, тег = 1. Вища оцінка перемагає. Однакові оцінки: перемагає правило, що стоїть пізніше у файлі стилів.
  • Правило вибору: клас для компонентів що повторюються, ID для одного унікального елемента на сторінці, комбінатори для цілювання за структурою DOM без зайвих класів.

Базовий приклад

css
p { color: red; } /* тег: усі p */ .note { background: yellow; } /* клас: можна використовувати повторно */ #main { font-weight: bold; } /* ID: найвища специфічність без inline */ div > p { color: green; } /* дочірній комбінатор: тільки прямі p в div */

div > p має специфічність 0-0-0-2 (два теги), а p — 0-0-0-1. Green перебиває red на будь-якому p, що є прямим нащадком div. Вкладений на два рівні p залишається червоним. Саме для цього існує дочірній комбінатор.

Типи селекторів

Базові:

  • p (тег) - знаходить усі елементи з цим тегом
  • .btn-primary (клас) - перевикористовуваний, стандартний вибір для UI-компонентів
  • #header (ID) - унікальний на сторінці, специфічність 100
  • * (універсальний) - знаходить усе; використовуй тільки в обмежених скидах на кшталт *, *::before, *::after { box-sizing: border-box; }

Комбінатори:

  • div p (нащадок) - будь-який p всередині div на будь-якій глибині
  • div > p (дочірній) - тільки прямі p нащадки div
  • h1 + p (сусідній) - єдиний p одразу після h1
  • h1 ~ p (загальний сусід) - усі p після h1 на тому ж рівні

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

  • [type="text"] - точне значення атрибута
  • [class~="foo"] - точне слово у списку розділеному пробілами
  • [href*="cdn"] - підрядок будь-де у значенні
  • [src^="https"] - значення починається з рядка
  • [src$=".svg"] - значення закінчується на рядок

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

  • :hover, :focus, :disabled - стан елемента
  • :nth-child(2n), :first-child - позиція серед сусідніх елементів
  • ::before, ::after - згенерований контент перед або після елемента

Головна різниця: базові vs комбінатори

Базові селектори знаходять елементи за тим, що вони є. Комбінатори знаходять за тим, де вони стоять у дереві. ul > li.active стилізує тільки активні пункти списку першого рівня і ігнорує li.active у вкладених списках. Без дочірнього комбінатора довелося б додавати окремий клас або писати JavaScript. У реальних проектах заміна .container p на .container > p часто прибирає десятки непередбачених перевизначень за одну правку.

Коли використовувати

  • Всі елементи одного типу - тег (p, h1, a)
  • Компоненти що повторюються - клас (.btn, .card)
  • Один унікальний елемент - ID (#skip-nav для доступності)
  • Цілювання за структурою DOM - дочірній комбінатор (nav > ul > li)
  • Динамічні стани - псевдоклас (:hover, :focus-visible, :disabled)
  • Парні рядки таблиці - :nth-child(2n) на tbody tr
  • Уникай - глибоких ланцюгів нащадків на кшталт div div div p; специфічністю важко керувати і зіставлення повільніше на великих DOM

Як браузер знаходить елементи

Браузер обробляє селектор справа наліво. Для nav > ul > li.active рушій спочатку знаходить усі .active елементи, перевіряє чи батько є ul, потім чи цей ul є прямим нащадком nav. Менше переходів вгору по дереву — швидше зіставлення. Blink у Chrome кешує результати в StyleResolver, тому перемальовування пропускає повторне зіставлення для незмінених вузлів.

Специфічність зберігається як три числа (кількість ID, класів, тегів). Коли два правила цілять в одну властивість одного елемента, перемагає вища оцінка. Однакові оцінки — перемагає правило що стоїть пізніше у файлі стилів. Про те як каскад і специфічність взаємодіють між собою, читай у CSS каскад і специфічність.

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

Нащадок замість дочірнього:

css
/* Знаходить кожен p всередині .container на будь-якій глибині */ .container p { color: blue; } /* Знаходить тільки прямі p: швидше і передбачуваніше */ .container > p { color: blue; }

Версія з нащадком чіпляє вкладені компоненти, які ти не планував стилізувати. І специфічністю потім важче керувати.

Зловживання ID-селекторами для стилізації:

css
/* Ламається якщо з'являється другий #header (невалідний HTML, але трапляється) */ #header p { display: none; } /* Клас добре поєднується з іншими правилами */ .site-header p { display: none; }

Специфічність ID (100) важко перебити без ще одного ID або !important. Класи (10) простіше комбінувати у великих проектах.

Видалення контуру фокусу без заміни:

css
/* Прибирає індикатор клавіатурної навігації, порушення WCAG */ button:hover { outline: none; } /* Показує контур тільки при клавіатурній навігації, не при кліку мишею */ button:focus-visible { outline: 2px solid currentColor; }

!important для вирішення конфліктів специфічності:

css
/* Запускає ланцюгову реакцію: наступний розробник додасть ще !important */ p { color: red !important; } /* Краще підвищити специфічність через структуру */ body .content p { color: red; }

Плутанина між :nth-child і :nth-of-type:

html
<ul> <div>Не список</div> <li>Перший li, другий дочірній</li> <!-- відповідає li:nth-child(2) --> <li>Другий li, третій дочірній</li> <!-- відповідає li:nth-of-type(2) --> </ul>

li:nth-child(2) знаходить елемент, який є другим дочірнім І є li. li:nth-of-type(2) знаходить другий li незалежно від інших тегів між ними. Використовуй :nth-of-type коли в батьківському елементі є теги різних типів.

Де використовується

  • React / styled-components - класи на кшталт .btn--primary для тем
  • Tailwind - довільні селектори [data-state=open] > * для акордеонів
  • Bootstrap - комбінатори navbar > .container для адаптивного лейауту
  • Material-UI - псевдокласи button:disabled для стилів за станом
  • Доступна навігація - :focus-visible замість :focus для всіх інтерактивних елементів

Питання на співбесіді

Q: Як рахується специфічність для #id .class p?
A: ID = 100, клас = 10, тег = 1. Разом = 111. Для .class p це 11. Правило з ID перемагає незалежно від позиції у файлі стилів.

Q: У чому різниця у продуктивності між дочірнім > і нащадком (пробіл)?
A: Дочірній перевіряє тільки безпосереднього батька. Нащадок обходить увесь ланцюг предків. Різниця відчутна на сторінках з 10 000+ вузлів — Chrome DevTools показує це в профайлері перерахунку стилів.

Q: Чим :nth-child відрізняється від :nth-of-type?
A: :nth-child(n) рахує всіх сусідів незалежно від тегу. :nth-of-type(n) рахує тільки сусідів того ж типу. Використовуй :nth-of-type коли в батькові є теги різних типів.

Q: Напиши селектор для кожного другого рядка в тілі таблиці, ігноруючи заголовок.
A: tbody tr:nth-child(2n). Область tbody автоматично виключає thead, тому додатковий фільтр не потрібен. Працює коректно навіть для таблиць з тисячами рядків.

Q: Яка різниця між [class~="foo"] і [class*="foo"]?
A: ~= знаходить точне слово у списку розділеному пробілами. class="foo bar" відповідає, class="foobar" — ні. *= шукає підрядок, тому обидва відповідають. Використовуй ~= коли потрібна точна межа слова.

Приклади

Каскад базових селекторів

html
<style> p { color: red; } /* 0-0-1 */ .note { background: yellow; } /* 0-1-0 */ #main { font-weight: bold; } /* 1-0-0 */ div > p { color: green; } /* 0-0-2 */ </style> <p>Червоний текст.</p> <p class="note">Жовтий фон, червоний текст.</p> <div> <p>Зелений текст (дочірній комбінатор перемагає тег).</p> </div> <div id="main"> <p>Зелений текст, жирний (два правила, різні властивості).</p> </div>

Специфічність div > p (0-0-2) перевищує p (0-0-1) для властивості color, тому прямі нащадки div стають зеленими. Правило #main цілить у div, а не в p, тому font-weight: bold успадковується вниз. Саме тут успадковані стилі можуть збивати з пантелику: жирний шрифт видно на p у devtools, але відповідне правило стоїть на div.

Дочірній комбінатор у React-компоненті навігації

jsx
// Nav.jsx const Nav = () => ( <nav> <ul> <li className="active">Dashboard</li> <li>Reports</li> </ul> <ul> <li className="active">Sub-item</li> </ul> </nav> );
css
/* nav.css */ nav > ul > li.active { background: #007bff; color: white; }

Обидва ul є прямими нащадками nav, тому обидва li.active відповідають правилу. Якщо потрібно стилізувати тільки верхню навігацію, краще додати клас до першого ul замість того щоб покладатися на позицію. Ланцюг комбінаторів звужує ціль, але не гарантує унікальності. Знай структуру свого HTML перед тим як писати селектор.

Тонкощі специфічності і nth-child

html
<style> li:nth-child(2) { color: red; } /* специфічність 0-1-1 */ li:nth-of-type(2) { color: blue; } /* специфічність 0-1-1 */ </style> <ul> <div>Не список</div> <!-- дочірній 1 --> <li>Перший li (дочірній 2)</li> <!-- red: відповідає nth-child(2) --> <li>Другий li (дочірній 3)</li> <!-- blue: відповідає nth-of-type(2) --> </ul>

Обидва правила мають специфічність 0-1-1. Перший li є другим дочірнім загалом, тому спрацьовує :nth-child(2). Другий li є другим за типом, тому спрацьовує :nth-of-type(2). Конфлікту немає. Але в таблиці tbody tr:nth-child(2) відлік починається від першого tr у tbody, а не з початку всієї таблиці. Обмеження через tbody скидає лічильник. Це та деталь, яка стає пасткою на реальних співбесідах.

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

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

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

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