Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «CSS селектори». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CSS селектори** - патерни для знаходження HTML елементів за тегом, класом, ID, атрибутом або позицією в DOM. Типи: `p` (тег), `.class`, `#id`, `[attr]`, `:hover` (псевдоклас), `::before` (псевдоелемент). Комбінатори: `>` (дочірній), `+` (сусідній), `~` (загальний сусід), пробіл (нащадок). **Ключове:** специфічність (specificity) визначає яке правило перемагає коли два селектори цілять в один елемент.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 каскад і специфічність](/questions/css-cascade). ### Типові помилки **Нащадок замість дочірнього:** ```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` скидає лічильник. Це та деталь, яка стає пасткою на реальних співбесідах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.