Як Каскад обирає переможця: важливість, конкретність та порядок джерел
Дізнайтеся, як браузери вирішують суперечливі оголошення CSS, як читати специфічність через чотириетапне порівняння, та чому правила hover та !important так часто дивують вас.
Коли кілька правил CSS націлені на один і той самий елемент та встановлюють одну й ту саму властивість, браузер не може застосувати їх усі. Йому потрібен детермінований спосіб для вибору саме однієї заяви, і цей процес пояснює цілу категорію помилок типу „Чому мій стиль ігнорується?“. До кінця цього посібника ви зможете розраховувати специфічність селектора, передбачати, яка заява виграє, та відлагоджувати складні випадки, такі як правило :hover, яке ніколи не активується, без використання !important.
Місце цього у процесі відображення
На високому рівні браузер перетворює маркап та стилі на пікселі через низку етапів. HTML парсується у DOM, CSS — у CSSOM, обидва об’єкти поєднуються у дерево відображення, а потім етапи розташування та малювання створюють те, що ви бачите:
HTML
↓
DOM
↓
CSS
↓
CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
Pixels
Усередині механізму CSS step приховано три взаємопов’язані запитання. По-перше, коли конкурують кілька декларацій, яка з них перемагає? По-друге, після вибору переможця до чого насправді призводить його значення? По-третє, що відбувається, коли у елемента зовсім немає значення для певної властивості? Відповідями є каскадування (де ключовою є специфічність), обробка значень та успадкування. Їх часто викладають як окремі теми, але у браузері це послідовні кроки однієї процедури. Цей посібник присвятований першому запитанню. Якщо ви хочете дізнатися більше про те, що відбувається після обробки стилів, перегляньте як браузер малює елементи та яке місце займає React.
Правила, декларації та конкуруючі значення
Спочатку трохи лексикології. Правило CSS складається з селектора, який слідує за блоком оголошень:
.button {
background-color: blue;
}
У цьому правилі .button є селектором, background-color: blue; — це оголошення, background-color — властивість, а blue — її значення. Саме те значення, як ви його написали, називається оголошеним значенням.
Справжній стильовий файл часто містить кілька оголошень для однієї й тієї самої властивості у одному елементі. Одне правило може стосуватися всіх кнопок:
button {
background-color: red;
}
тоді як інші, можливо, у різному файлі, стосуються кнопок загалом та однієї конкретної кнопки за її id:
button {
background-color: blue;
}
#submit {
background-color: green;
}
Якщо один елемент <button id="submit"> відповідає усім трьом правилам, який колір фону він має отримати? Вирішення цього питання — завдання каскадування. Воно усуває конфлікти, враховуючи значимість кожної декларації, специфічність її селектора та порядок, у якому вони подані. Після того, як була врахована значимість, зазвичай саме специфічність визначає кінцевий результат.
У повній специфікації каскадування також враховується походження стилевої таблиці (стандартні налаштування браузера, користувацькі стилі, стилі автора) та, у сучасному CSS, рівні каскадування, оголошені за допомогою @layer. Для звичайних стилевих таблиць авторів без рівнів каскадування ключовими факторами для прийняття рішень є значимість, специфічність та порядок джерел.
Що вимірює специфічність
Специфічність — це показник браузера щодо того, наскільки точно селектор вказує на елемент. Не всі селектори мають однакову вагу. Порівняйте селектор типу:
p {
color: red;
}
селектор класу:
.text {
color: blue;
}
та селектор id:
#title {
color: green;
}
Якщо всі три відповідають одному й тому ж елементу, браузер ранжує їх за фіксованою ієрархією типів селекторів, від найсильнішого до найслабкішого:
Inline styles
↓
IDs
↓
Classes / pseudo-classes / attributes
↓
Elements / pseudo-elements
Коли конкуруючі декларації мають однакову важливість, перемагає більш специфічний селектор. У цьому випадку переможе правило id, і текст буде зеленим.
Важлива примітка щодо верхнього рядка: стилі, встановлені безпосередньо у елементі через атрибут style, не є селекторами, і поточні специфікації розглядають їх як окремий крок, який має пріоритет над будь-якою декларацією автора, заснованою на селекторах. Хоча їх зазвичай моделюють як найвищу „колонку“ специфічності, що дає такий самий результат на практиці, у решті цього посібника використовується саме ця зручна модель.
Розуміння специфічності як чотирьох колонок
Inline | IDs | Classes | Elements
Для заданого селектора підраховується, скільки елементів належить до кожної категорії. Найпростішим випадком є селектор класу:
.button {
background: blue;
}
Він не містить жодних стилів безпосередньо у елементі, жодного id, лише один клас та жодних елементів:
Inline styles → 0
IDs → 0
Classes → 1
Elements → 0
що можна компактно записати як:
0, 0, 1, 0
Тепер розглянемо більш складний селектор:
nav#main .button div {
background: green;
}
Він містить один id (#main), одну класифікацію (.button) та два селектори типу (nav та div), тож його специфічність становить:
0, 1, 1, 2
Щоб порівняти два селектори, браузер читає ці туплі зліва направо, починаючи з найважливішого стовпця. Перший стовпець, у якому вони відрізняються, визначає результат, а стовпці праворуч вже не мають значення. Саме тому один id переважає будь-яку кількість класифікацій, а одна класифікація — будь-яку кількість селекторів типу: немає перенесення значень з одного стовпця в інший, тому десять класифікацій ніколи не „додадуться“ до одного id. Вам не потрібно запам’ятовувати арифметику; потрібно пам’ятати порядок порівняння.
Розгляд реального конфлікту
Розгляньмо кнопку, яка має як клас, так і id. Зауважте, що це звичайний HTML-маркап, тому використовується class, а не className від JSX:
<button class="button" id="submit">
Don't Click
</button>
Тепер припустимо, що стилівий файл містить правило для класу:
.button {
background: blue;
}
а також кілька інших правил, зокрема вибірник типу, довгий вибірник нащадків та правило id-plus-class зі станом hover:
button {
background: purple;
}
nav#main .button div {
background: green;
}
#submit.button:hover {
background: yellow;
}
Усі вони визначають background, тому конкурують між собою. Браузер не просто бере те, що знаходиться останнім; спочатку він порівнює специфічність.
У цьому блоку є одна деталь, яку легко пропустити: nav#main .button div насправді вказує на div, розташований всередині елемента з класом button, а не на сам кнопку. Оскільки об’єктом селектора є його найправіша частина, це правило ніколи не буде відповідати нашому <button>, незалежно від його специфічності. Перевірка того, чи взагалі правило відповідає, завжди є першим кроком у відлагоджуванні.
Для правил, які все ж таки відповідають, порівняйте селектор класу:
.button
зі звичайним селектором типу:
button
У першому є один клас, у другому — лише один елемент. Отже:
.button
має вищий пріоритет:
button
і кнопка синя, а не фіолетова, хоча правило для фіолетового кольору з’являється пізніше.
Додайте id до схеми:
#submit.button
id розміщує цей селектор у вищому стовпці, ніж будь-який селектор, створений лише з класів та типів. Порівнюючи зліва направо, стовпець id відразу вирішує справу, тому один id може перевершити селектор, складений з багатьох класів.
Чому правильне правило :hover може нічого не зробити
Цей випадок спричиняє багато плутанини під час дебагування. Почніть з базового правила для кнопки:
#submit.button {
background: red;
}
та правила hover для того ж елемента:
#submit.button:hover {
background: yellow;
}
Правило hover містить один id, один клас та одну псевдоклас, що надає йому більшої специфічності, ніж базове правило, тому при наведенні курсору кнопка стає жовтою, як і очікувалося.
Тепер уявіть, що інша частина кодової бази форматує ту саму кнопку за допомогою набагато довшого селектора:
nav#main div#container #submit.button {
background: red;
}
при цьому правило hover залишається незмінним:
#submit.button:hover {
background: yellow;
}
Правило hover все ще містить псевдоклас:
:hover
Псевдокласи також враховуються у стовпці з класами. Але це додає лише один бал на рівні класу. Довгий селектор містить три ідентифікатори, на відміну від одного у правилі hover, тож він перемагає у стовпці з ідентифікаторами ще до порівняння класів. Результатом є правило :hover, яке є синтаксично ідеальним, відповідає елементу, але все одно нічого не змінює на екрані.
Рівність розв’язується за порядком викладення
Іноді два селектори мають ідентичну специфічність. Візьмемо два правила з однаковим селектором класу:
.button {
background: red;
}
.button {
background: blue;
}
Вони однаково конкретні та однаково важливі, тому жоден із перших двох рівнів не може прийняти рішення. Тоді браузер переходить до порядку вказівок: перемагає та декларація, яка з’являється пізніше. За таким порядком:
.button {
background: red;
}
.button {
background: blue;
}
кнопка в кінцевому підсумку стає синьою.
Весь процес прийняття рішення можна уявити як послідовність правил для вирішення нічиї:
Importance
↓
Specificity
↓
Source Order
Кожен рівень враховується лише тоді, коли попередній не зміг визначити переможця. Спочатку оцінюється важливість; якщо є нічия, — конкретність; якщо й тут нічия, — перемагає остання декларація.
Витрати використання !important
Майже кожен розробник використовував цей вихід принаймні один раз:
color: red !important;
Додавання !important підвищує пріоритет оголошення. Оскільки пріоритет перевіряється перед специфічністю, оголошення, позначене таким чином, може перемогти інше оголошення з набагато вищою специфічністю. Наприклад:
.button {
background: purple !important;
}
воно переможе звичайне оголошення у випадку довгого селектора з багатьма ідентифікаторами. Коли два оголошення з !important конкурують, браузер знову порівнює їхню специфічність, а потім порядок їх розташування.
Саме ця можливість робить її ризикованою. Знайома спіраль дебагування виглядає ось так:
"My style isn't working."
↓
"Let's increase the specificity."
↓
"Still not working."
↓
"Let's add !important."
↓
"It works!"
Стиль нарешті проявляється, але основний конфлікт не зник; він переходить до наступної особи, якій потрібно перевизначити цю властивість, і тепер їй доводиться боротися зі своїм власним !important. Чим більше таких випадків накопичується, тим складніше стає розуміти стилеву таблицю. Використовуйте !important лише як останній засіб, а раптова потреба в ньому — як сигнал про те, що CSS, ймовірно, потребує рефакторингу.
Перш ніж щось писати:
!important
задайте більш корисне запитання: чому моя декларація не перемагає? Потім перевірте рівні в такому порядку:
- Чи є конкуруюча декларація більш важливою?
- Чи є конкуруючий селектор більш специфічним?
- Чи з’являється конкуруюча декларація пізніше у коді?
Існують законні способи використання, такі як класи-функції, призначені для постійного застосування стилів, або перевизначення вбудованих стилів, які додає сторонній віджет та які неможливо змінити, але їх використання має бути усвідомленим, а не автоматичним.
Створення селекторів, які природним чином перемагають
Тут також існує питання зберіганості коду. Коли стиль не застосовується, виникає спокуса робити селектор все довшим і довшим, поки він не почне працювати:
body div section nav ul li a.button {
color: red;
}
Це працює, але кожна додаткова частина підвищує вимоги до майбутнього перевизначення, прив’язує стиль до конкретної структури DOM та ускладнює читання таблиці стилів. Замість того, щоб питати, як змусити селектор перемогти за будь-яку ціну, потрібно замислитися над тим, як організувати CSS так, щоб бажана декларація сама по собі мала перевагу. Використання одного класу для більшості селекторів, уникнення ідентифікаторів для стилізації та групування більш специфічних перевизначень поруч із правилами, які вони змінюють, допомагає у цьому.
Це має найбільше значення у великих кодових базах, де багато людей пишуть CSS. Специфічність призначена для того, щоб результати були прогнозованими, а не для того, щоб стати змаганням у кількості селекторів.
Використання порядку джерел із стилевими таблицями третіх сторін
Порядок джерел стає практичним інструментом, коли ви поєднуєте власні стилі з файлом для скидання налаштувань чи стилевою таблицею третьої сторони. Ці файли зазвичай встановлюють стилі для поширених елементів, і ви хочете, щоб ваші правила їх переважали. Завантаження вашої стилевої таблиці після їхньої ускладнює це, тож:
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">
Коли ваші селектори мають таку саму специфічність, як і селектори бібліотеки, перемагає файл, який завантажується пізніше, тому розміщення style.css після reset.css дозволяє вашим оголошенням набути чинності без збільшення кількості селекторів.
Залежність від порядку також має свою ціну. Якщо хтось пізніше знову вказує теги <link> або елементи імпорту в точці входу бандлера, стилі можуть змінитися непомітно. Там, де це можливо, варто обирати правила, чия специфічність чітко визначає переможця, та використовувати порядок джерел як остаточний критерій рішення, для чого він і був створений. Шари каскадування, якщо ваші цільові браузери їх підтримують, — це більш чіткий спосіб позначити, що «спочатку бібліотека, а потім наші стилі».
Після переможця: каскадоване значення
На цьому етапі вже відповідається на перше запитання. Браузер зібрав усі оголошення для властивості, оцінив їхню важливість, потім специфічність та нарешті порядок джерел, і обрав одне. Це переможне значення називається каскадованим значенням.
Проте робота ще не завершена. Припустимо, переможним оголошенням є:
width: 66%;
Такий відсоток не можна намалювати безпосередньо; браузер все одно мусить його обробити, визначивши фактичну довжину шляхом порівняння з контейнерним блоком. Ця обробка значень, разом із успадкуванням властивостей, які зовсім не оголошені, є наступним етапом перетворення CSS у пікселі.
Ключові моменти
- Механізм каскадування вирішує конфлікти у фіксованому порядку: спочатку за важливістю, потім за специфічністю, а далі — за порядком джерел.
- Специфічність визначається за допомогою чотирьох критеріїв (inline, ідентифікатори, класи/псевдокласи/атрибути, елементи/псевдоелементи), які переглядаються зліва направо; значення з нижчих критеріїв ніколи не впливають на критерії з вищих рядків.
- Перед порівнянням специфічності необхідно переконатися, що правило справді відповідає елементу; найправіша частина селектора визначає об’єкт, до якого він стосується.
:hover або інша псевдокласова правило додає лише вагу на рівні класу та може програти більш конкретному базовому правилу.!important перемагає шляхом зміни рівня важливості, а не шляхом усунення конфлікту; використовуйте його обережно та лише у необхідних випадках.