Специфічність

Специфічність – це алгоритм, що використовується браузерами для визначення того оголошення CSS, яке найбільше відповідає елементові. І котре, своєю чергою, визначає те значення властивості, яке буде застосовано до елемента. Алгоритм специфічності обчислює вагу селектора CSS, щоб визначити, яке правило серед суперницьких оголошень CSS застосовується до елемента.

Примітка: Браузери розглядають специфічність після визначення каскадних джерела та важливості. Інакше кажучи, серед суперницьких властивостей специфічність грає роль та порівнюється лише між селекторами з одними каскадними джерелом та шаром, що мають пріоритет для властивості. Наближеність області видимості та порядок появи грають роль, коли специфічності селекторів суперницьких оголошень в межах каскадного шару з пріоритетом є рівними.

Як обчислюється специфічність

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

Алгоритм специфічності по суті є триколонковим порівнянням значень трьох категорій чи ваг – ID, CLASS та TYPE – що відповідають трьом типам селекторів. Це значення представляє кількість компонентів селектора з кожної вагової категорії та записується як ID - CLASS - TYPE. Три колонки утворюються шляхом підрахунку кількості компонентів з кожної вагової категорії у селекторах, що відповідають елементові.

Вагові категорії селекторів

Тут перелічені вагові категорії селекторів у порядку спадання специфічності:

колонка ID

Включає лише селектори ідентифікатора виду #example. Для кожного ID у селекторі збігу – додати до значення ваги 1-0-0.

колонка CLASS

Включає селектори класу виду .myClass, селектори атрибута виду [type="radio"] і [lang|="fr"] та псевдокласи, наприклад, :hover, :nth-of-type(3n) і :required. Для кожного класу, селектора атрибута чи псевдокласу у селекторі збігу – додати до значення ваги 0-1-0.

колонка TYPE

Включає селектори типу виду p, h1 і td, а також псевдоелементи виду ::before, ::placeholder і всі інші селектори з синтаксисом подвійної двокрапки. Для кожного типу чи псевдоелементу в селекторі збігу – додати до значення ваги 0-0-1.

Без значення

Універсальний селектор (*) та псевдоклас :where() і їх параметри – не враховуються при підрахунку ваги, тож їхнє значення – 0-0-0, хоч вони й дають збіг з певними елементами. Ці селектори не впливають на значення ваги специфічності.

Комбінатори, наприклад, +, >, ~, " " і ||, можуть зробити селектор більш специфічним щодо того, що йому відповідає, але не додають жодного значення до ваги специфічності.

Комбінатор вкладеності & не додає ваги специфічності, а вкладені правила навпаки – додають. Щодо специфічності та функціональності, то вкладеність вельми подібна до псевдокласу :is().

Подібно до вкладеності, псевдокласи :is(), :has() і заперечення (:not()) самі собою не мають ваги. Проте параметри в їхніх селекторах - мають. Вага специфічності кожного з них надходить з параметра-селектора в списку селекторів, що має найвищу специфічність. Подібно до цього, для вкладених селекторів вага специфічності, додана вкладеним компонентом-селектором, – це селектор у сполученому комами списку вкладених селекторів, що має найвищу специфічність.

Про винятки :not(), :is(), :has() і вкладеності CSS – нижче.

Відповідний селектор

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

[type="password"],
input:focus,
:root #myApp input:required {
  color: blue;
}

Селектор [type="password"] – нагорі списку селекторів, він має вагу 0-1-0, застосовує оголошення color: blue до всіх полів типу password.

Всі поля введення, незалежно від типу, отримуючи фокус, відповідають другому селекторові в списку, input:focus, що має вагу специфічності 0-1-1; ця вага складена псевдокласом :focus (0-1-0) та типом input (0-0-1). Якщо поле введення пароля має фокус, то відповідатиме input:focus, і вага специфічності оголошення стилю color: blue буде 0-1-1. Коли таке поле пароля не має фокусу, то вага специфічності залишається 0-1-0.

Специфічність обов'язкового поля введення, вкладеного в елемент з атрибутом id="myApp", дорівнюватиме 1-2-1, на основі одного ідентифікатора, двох псевдокласів та одного типу елемента.

Якщо поле типу пароля, на якому задано required, вкладене в елемент з id="myApp", то вага специфічності буде 1-2-1 – на основі одного ідентифікатора, двох псевдокласів та одного типу елемента, незалежно від того, чи має поле фокус. Чому вага специфічності в такому випадку 1-2-1, а не 0-1-1 чи 0-1-0? Через те, що вага специфічності випливає з селектора збігу з найбільшою вагою специфічності. Вага визначається порівнянням значень у трьох колонках, зліва направо.

[type="password"]             /* 0-1-0 */
input:focus                   /* 0-1-1 */
:root #myApp input:required   /* 1-2-1 */

Триколонкове порівняння

Щойно визначені значення специфічності відповідних селекторів, порівнюється число компонентів селектора в кожній колонці, зліва направо.

#myElement {
  color: green; /* 1-0-0  - ПЕРЕМАГАЄ!! */
}
.bodyClass .sectionClass .parentClass [id="myElement"] {
  color: yellow; /* 0-4-0 */
}

Перша колонка – значення компонента ID, що є числом ідентифікаторів у кожному селекторі. Порівнюються числа в колонках ID суперницьких селекторів. Селектор з найбільшим значенням в колонці ID перемагає незалежно від значень в інших колонках. У прикладі вище, навіть попри те, що селектор жовтого кольору має в цілому більше компонентів, грає роль лише значення в першій колонці.

Якщо числа в колонках ID суперницьких селекторів – однакові, то порівнюється наступна колонка, CLASS, як показано нижче.

#myElement {
  color: yellow; /* 1-0-0  */
}
#myApp [id="myElement"] {
  color: green; /* 1-1-0  - ПЕРЕМАГАЄ!! */
}

Колонка CLASS – число в селекторі імен класів, селекторів атрибута та псевдокласів. Якщо значення колонки ID – однакове, то перемагає селектор з більшим значенням в колонці CLASS, незалежно від значення в колонці TYPE. Це показано в прикладі нижче

:root input {
  color: green; /* 0-1-1 - ПЕРЕМАГАЄ, адже значення в колонці CLASS – більше */
}
html body main input {
  color: yellow; /* 0-0-4 */
}

Якщо числа в колонках CLASS і ID суперницьких селекторів однакові, то стає важливою колонка TYPE. Ця колонка TYPE містить число типів елемента і псевдоелементів у селекторі. Коли перші дві колонки мають однакове значення, то перемагає селектор із більшим числом у колонці TYPE.

Якщо суперницькі селектори мають однакові значення у всіх трьох колонках, то у гру вступає правило наближення, згідно з яким останній оголошений стиль отримує перевагу.

input.myClass {
  color: yellow; /* 0-1-1 */
}
:root input {
  color: green; /* 0-1-1  ПЕРЕМАГАЄ, бо стоїть останнім */
}

Винятки – :is(), :not(), :has() і вкладеність CSS

Псевдоклас збігу з будь-чим :is(), псевдоклас відносності :has() та псевдоклас заперечення :not() не розглядаються як псевдокласи при обчисленні ваги специфічності. Самі по собі вони не додають жодної ваги до рівняння специфічності. Проте параметри-селектори, передані в дужки псевдокласів, є частиною алгоритму специфічності; вагою псевдокласу збігу з будь-чим та псевдокласу заперечення при обчисленні значення специфічності є вага параметра.

p {
  /* 0-0-1 */
}
:is(p) {
  /* 0-0-1 */
}

h2:nth-last-of-type(n + 2) {
  /* 0-1-1 */
}
h2:has(~ h2) {
  /* 0-0-2 */
}

div.outer p {
  /* 0-1-2 */
}
div:not(.inner) p {
  /* 0-1-2 */
}

Зверніть увагу, що у спарюванні CSS вище вага специфічності, надана псевдокласами :is(), :has() і :not(), є значеннями селектора-параметра, а не псевдокласу.

Усі ці три псевдокласи приймають як параметр складні списки селекторів, список розділених комами селекторів. Ця особливість може використовуватись для збільшення специфічності селектора:

:is(p, #fakeId) {
  /* 1-0-0 */
}
h1:has(+ h2, > #fakeId) {
  /* 1-0-1 */
}
p:not(#fakeId) {
  /* 1-0-1 */
}
div:not(.inner, #fakeId) p {
  /* 1-0-2 */
}

У блоці CSS вище #fakeId включено у всі селектори. Цей #fakeId додає 1-0-0 до ваги специфічності кожного параграфа.

При утворенні складних списків селекторів з вкладеним CSS це поводиться точно так само, як псевдоклас :is().

p,
#fakeId {
  span {
    /* 1-0-1 */
  }
}

У блокові коду вище складний селектор p, #fakeId має специфічність, взяту із #fakeId, а також span, тож це дає специфічність 1-0-1 для p span і #fakeId span. Це специфічність, котра рівносильна специфічності селектора :is(p, #fakeId) span.

Загалом краще зводити специфічність до мінімуму, але якщо треба з певної причини збільшити специфічність елемента, ці три псевдокласи можуть допомогти.

a:not(#fakeId#fakeId#fakeID) {
  color: blue; /* 3-0-1 */
}

У цьому прикладі всі посилання будуть блакитними, якщо це не відкинуто оголошенням посилання з трьома чи більше ідентифікаторами, якщо значення кольору a не включає позначки !important і якщо посилання не має оголошення кольору у вбудованому стилі. При використанні однієї з цих технік слід додати коментар, чому такий викрут необхідний.

Вбудовані стилі

Вбудовані стилі, додані до елемента (наприклад, style="font-weight: bold;") завжди відкидають будь-які звичайні стилі в авторських таблицях стилів, а отже – можуть розглядатися як такі, що мають найвищу специфічність. Варто уявляти специфічність вбудованих стилів як 1-0-0-0.

Єдиний спосіб відкинути вбудовані стилі – використати !important.

Чимало фреймворків та бібліотек JavaScript додає вбудовані стилі. Використання !important із вельми вузькоспрямованим селектором, наприклад, селектором атрибута за допомогою вбудованого стилю, є одним зі способів відкинути такі вбудовані стилі.

<p style="color: purple"></p>
p[style*="purple"] {
  color: rebeccapurple !important;
}

Слід пересвідчитися, що кожна поява позначки !important супроводжується коментарем, щоб відповідальні за супровід коду розуміли, чому використовується антипатерн CSS.

Виняток !important

Оголошення CSS, позначені як важливі, відкидають будь-які інші оголошення в межах тих самих каскадних шару та джерела. Утім, хоч технічно !important не має стосунку до специфічності, вона напряму взаємодіє зі специфічністю та каскадом. !important обертає каскадний порядок таблиць стилів.

Якщо оголошення з одними джерелом та каскадним шаром конфліктують, і одне значення властивості має позначку !important, то таке важливе оголошення застосовується незалежно від специфічності. Коли конфліктні оголошення з одних джерела та каскадного шару із позначками !important застосовуються до одного елемента, застосовується оголошення із більшою специфічністю.

Використання !important для відкидання специфічності вважається шкідливою практикою, його слід уникати. Розуміння та ефективне використання специфічності та каскадності може усунути будь-яку потребу використання позначки !important.

Замість використання позначки !important для відкидання чужого CSS (зі сторонніх бібліотек штибу Bootstrap чи normalize.css), краще імпортувати сторонні сценарії напряму в каскадні шари. Якщо ви мусите використати !important у своєму CSS, то прокоментуйте таке використання, щоб відповідальні за підтримку коду в майбутньому знали, чому оголошення було позначено як важливе, і що його не варто відкидати. Але !important однозначно не слід використовувати при написанні втулок чи фреймворків, котрі іншим розробникам треба буде застосовувати без змоги контролювати.

Виняток :where()

Специфічність псевдокласу припасування специфічності :where() завжди зводиться до нуля, 0-0-0. Це дає змогу робити селектори CSS украй конкретними щодо того, з якими елементами вони дають збіг, без збільшення їх значення специфічності.

При створенні CSS коду, яким користуватимуться розробники, котрі не матимуть доступу до редагування вашого CSS, корисною практикою вважається створювати CSS із найменшою можливою специфічністю. Наприклад, якщо ваша тема містить наступний CSS:

:where(#defaultTheme) a {
  /* 0-0-1 */
  color: red;
}

То розробник, що розробляє віджет, може легко відкинути колір посилання, використовуючи самі селектори типу.

footer a {
  /* 0-0-2 */
  color: blue;
}

Як блоки @scope впливають на специфічність

Додання набору правил усередині блоку @scope не впливає на специфічність його селектора, незалежно від селекторів, що використовуються усередині кореня та обмеження області видимості. Наприклад:

@scope (.article-body) {
  /* img має специфічність 0-0-1, як і очікується */
  img { ... }
}

Проте якщо вирішити явно поставити перед обласним селектором псевдоклас :scope, то треба врахувати його при обчисленні специфічності. Псевдоклас :scope, як і всі звичайні псевдокласи, має специфічність 0-1-0. Наприклад:

@scope (.article-body) {
  /* :scope img має специфічність 0-1-0 + 0-0-1 = 0-1-1 */
  :scope img { ... }
}

Коли використати селектор & усередині блоку @scope, то & представляє селектор кореня області видимості; він за лаштунками переписується на цей селектор, обгорнутий у селектор :is(). Тож, наприклад, у:

@scope (figure, #primary) {
  & img { ... }
}

Селектор & img еквівалентний :is(figure, #primary) img.

Оскільки :is() набуває специфічності свого найбільш специфічного аргументу (#primary, у цьому випадку), то специфічність обласного селектора & img дорівнює 1-0-0 + 0-0-1 = 1-0-1.

Поради щодо боротьби з каверзами специфічності

Замість використання !important варто розглянути варіант застосування каскадних шарів та використання у вашому CSS низької ваги специфічності, щоб стилі легко відкидалися трохи більш специфічними правилами. Вживання семантичного HTML допомагає надавати опори, спираючись на котрі, можна застосувати оформлення.

Специфічні селектори із високою та низькою специфічністю

Вказуючи секцію документа, котра оформлюється, перед елементом, що відповідатиме селектору, ви робите правило більш специфічним. Залежно від того, як ви вказуєте секцію, можна додати трохи, багато чи нуль специфічності, як видно нижче:

<main id="myContent">
  <h1>Текст</h1>
</main>
#myContent h1 {
  color: green; /* 1-0-1 */
}
[id="myContent"] h1 {
  color: yellow; /* 0-1-1 */
}
:where(#myContent) h1 {
  color: blue; /* 0-0-1 */
}

Незалежно від порядку, заголовок буде зеленим, бо відповідне правило є найбільш специфічним.

Зниження специфічності селектора ідентифікатора

Специфічність заснована на формі селектора. Включення id елемента як селектора атрибута замість селектора ідентифікатора – добрий спосіб зробити елемент більш специфічним без додавання надлишкової специфічності. У попередньому прикладі селектор [id="myContent"] при визначенні специфічності селектора рахується як селектор атрибута, навіть попри те, що вказує на ідентифікатор.

Також можна включити id або будь-яку частину селектора як параметр псевдокласу припасування специфічності :where(), якщо треба зробити селектор конкретнішим, не додаючи йому жодної специфічності.

Збільшення специфічності шляхом дублювання селекторів

Як особливий випадок збільшення специфічності, можна подвоїти вагу колонок CLASS чи ID. Продублювавши селектори ідентифікатора, класу, псевдокласу чи атрибута, можна збільшити специфічність у межах складеного селектора, відкидаючи будь-які специфічні селектори, над котрими немає контролю.

#myId#myId#myId span {
  /* 3-0-1 */
}
.myClass.myClass.myClass span {
  /* 0-3-1 */
}

Використовуйте такий підхід ощадно; найкраще не використовувати його узагалі. Використовуючи дублювання селектора, завжди коментуйте свій CSS.

За допомогою :is() і :not() (а також :has()) можна збільшити специфічність навіть тоді, коли до батьківського елемента немає змоги додати id:

:not(#fakeID#fakeId#fakeID) span {
  /* 3-0-1 */
}
:is(#fakeID#fakeId#fakeID, span) {
  /* 3-0-0 */
}

Пріоритет над стороннім CSS

Використання каскадних шарів – стандартний спосіб надати одному набору стилів пріоритет над іншим; каскадні шари дають змогу це зробити без використання специфічності! Звичайні (не важливі) авторські стилі, імпортовані у каскадні шари, мають нижчий пріоритет, ніж непошаровані авторські стилі.

Якщо стилі надійшли з таблиці, котру ви не маєте змоги (або не знаєте як) редагувати — але потрібно перевизначити стилі, — є стратегія імпортування стилів, котрі ви не контролюєте, до каскадного шару. Стилі в шарах, оголошених нижче, мають пріоритет, а непошаровані стилі мають пріоритет над усіма пошарованими стилями того самого джерела.

Коли два селектори з двох різних шарів мають збіг з одним елементом, то пріоритет мають джерело та важливість; специфічність селектора в таблиці стилів, що програла, не грає ролі.

<style>
  @import TW.css layer();
  p,
  p * {
    font-size: 1rem;
  }
</style>

У прикладі вище увесь текст параграфа, включно зі вкладеним вмістом, буде 1rem незалежно від того, скільки параграфи матимуть імен класів, що даватимуть збіг з таблицею стилів TW.

Уникання та відкидання !important

Найкращий підхід – не використовувати !important. Пояснення специфічності вище повинні бути корисними для уникання використання такої позначки та її прибирання при зустрічі з нею.

Щоб усунути уявну потребу !important, можна зробити щось із наступного:

  • Збільшити специфічність селектора колишнього оголошення !important, щоб вона була більшою за таку в інших оголошень
  • Задати йому таку саму специфічність і поставити після оголошення, що має бути відкинутим
  • Знизити специфічність селектора, котрий намагаєтеся відкинути.

Всі ці методи розкриті у секціях вище.

Якщо ви не можете прибрати позначки !important з авторської таблиці стилів, єдиним рішенням є відкидання важливих стилів за допомогою !important. Створення каскадного шару з важливим оголошенням – чудове рішення. Серед двох способів це зробити:

Метод 1

  1. Створити окрему, коротку таблицю стилів, що містить лише важливі оголошення, котрі відкидають конкретно саме ті важливі оголошення, котрі не виходить прибрати.
  2. Імпортувати цю таблицю стилів як перший імпорт у вашому CSS за допомогою layer(), включаючи інструкцію @import, до під'єднання до інших таблиць стилів. Це необхідно, щоб пересвідчитися, що важливі відкидання імпортовані як перший шар.
<style>
  @import importantOverrides.css layer();
</style>

Метод 2

  1. На початку оголошень таблиці стилів створити іменований каскадний шар, ось так:
@layer importantOverrides;
  1. Щоразу при потребі відкинути важливе оголошення – оголошувати відкидання всередині іменованого шару. Оголошувати в такому шарі лишень важливі правила.
[id="myElement"] p {
  /* тут звичайні стилі */
}
@layer importantOverrides {
  [id="myElement"] p {
    /* тут важливий стиль */
  }
}

Специфічність селектора важливого стилю всередині шару може бути низькою, поки цей селектор дає збіг з елементом, котрий ви намагаєтесь відкинути. Звичайні шари повинні бути оголошені поза шаром, тому що пошаровані стилі мають нижчий пріоритет, ніж непошаровані.

Ігнорування наближеності по дереву

Наближеність елемента до інших елементів, на котрі є вказівки у даному селекторі, не впливає на специфічність.

body h1 {
  color: green;
}

html h1 {
  color: purple;
}

Елементи <h1> будуть фіолетовими, бо коли оголошення мають однакову специфічність, пріоритет має останній оголошений селектор.

Безпосередньо цільові елементи проти успадкованих стилів

Стилі безпосередньо цільового елемента завжди мають пріоритет над успадкованими стилями, незалежно від специфічності успадкованого правила. З такими CSS і HTML:

#parent {
  color: green;
}

h1 {
  color: purple;
}
<html lang="uk">
  <body id="parent">
    <h1>Ось вам заголовок!</h1>
  </body>
</html>

Елемент h1 буде фіолетовим, бо селектор h1 націлений на сам елемент, а зелений колір – успадкований від оголошень елемента #parent.

Приклади

В наступному CSS є три селектори, що встановлюють колір і націлені на елементи <input>. Для даного поля введення вага специфічності оголошення кольору, що має пріоритет – це селектор із найбільшою вагою, що дає збіг:

#myElement input.myClass {
  color: red;
} /* 1-1-1 */
input[type="password"]:required {
  color: blue;
} /* 0-2-1 */
html body main input {
  color: green;
} /* 0-0-4 */

Якщо усі селектори вище націлені на одне поле введення, то таке поле буде червоним, оскільки перше оголошення має найбільше значення у колонці ID.

Останній селектор має чотири компоненти TYPE. Хоч це найбільше числове значення, неважливо, скільки елементів та псевдоелементів включено, навіть якби 150, компоненти TYPE ніколи не мають пріоритету над компонентами CLASS. Коли значення у певних колонках є рівними, то порівняння колонок відбувається зліва направо.

Якщо перетворити селектор ідентифікатора у коді прикладу вище на селектор атрибута, то перші два селектори матимуть однакову специфічність, як показано нижче:

[id="myElement"] input.myClass {
  color: red;
} /* 0-2-1 */
input[type="password"]:required {
  color: blue;
} /* 0-2-1 */

Коли декілька оголошень мають рівну специфічність, застосовується оголошення, котре стоїть в CSS останнім. Якщо обидва селектори дають збіг з одним елементом <input>, то колір буде блакитним.

Додаткові примітки

Кілька речей, котрі слід пам'ятати про специфічність:

  1. Специфічність застосовується лише тоді, коли на один елемент націлені декілька оголошень в одному каскадному шарі чи джерелі. Специфічність грає роль лише для оголошень однакової важливості та одних джерела і каскадного шару. Якщо селектори, котрі дають збіг, перебувають в різних джерелах, то те, яке оголошення отримує пріоритет, визначає каскад.

  2. Коли два селектори в одних каскадному шарі та джерелі мають однакову специфічність, то тоді обчислюється наближеність областей видимості; правило з найменшою наближеністю області видимості перемагає. Дивіться подробиці та приклад у розділі Як розв'язуються конфлікти @scope.

  3. Якщо наближеність областей видимості обох селекторів однакова, то вступає в дію порядок у коді. Коли все інше однакове, перемагає останній селектор.

  4. Згідно з правилами CSS, безпосередньо цільові елементи завжди мають пріоритет над правилами, котрі елемент успадковує від свого предка.

  5. Наближеність елементів по дереву елементів не має впливу на специфічність.

Специфікації

Дивіться також