← JavaScript/CSS селекторы и специфичность#171 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

CSS селекторы и специфичность

Ты пишешь стиль .btn { color: red }, но кнопка по-прежнему синяя — потому что где-то есть #header .btn { color: blue } с большей специфичностью. Или в JavaScript: document.querySelector('form input:not([disabled]):first-child') — ты должен понимать что это вернёт. Специфичность и селекторы — это «язык приоритетов» в CSS.

Какую проблему решает

В больших проектах CSS-стили конфликтуют. Специфичность — правило, по которому браузер решает какой стиль применить. JavaScript использует CSS-селекторы для поиска элементов — чем лучше ты знаешь синтаксис, тем точнее запросы.

На основе предыдущих уроков

  • DOM: document.querySelector, querySelectorAll — принимают CSS-селекторы
  • RegExp: вычисление специфичности требует разбора строк
  • CSS: без понимания специфичности невозможно дебажить CSS
  • Типы селекторов

    // Базовые:
    // div            — по тегу
    // .foo           — по классу
    // #foo           — по ID
    // *              — все элементы
    
    // Атрибуты:
    // [type="text"]        — точное значение
    // [href^="https"]      — начинается с "https"
    // [src$=".png"]        — заканчивается на ".png"
    // [class*="btn"]       — содержит "btn"
    // [data-active]        — атрибут существует
    
    // Псевдоклассы:
    // :hover :focus :active :checked :disabled
    // :first-child :last-child :nth-child(2n+1)
    // :not(.hidden) :is(h1, h2, h3) :has(img)
    
    // Псевдоэлементы:
    // ::before ::after ::placeholder ::selection
    
    // Комбинаторы:
    // div p          — потомок (любой уровень)
    // div > p        — прямой дочерний
    // div + p        — следующий сосед
    // div ~ p        — все следующие соседи

    Специфичность — как браузер считает приоритет

    Специфичность записывается как тройка (id, class, element):

    // #header                → (1, 0, 0)   — выигрывает почти всегда
    // .btn.active            → (0, 2, 0)   — два класса
    // div.menu > li          → (0, 1, 2)   — класс + два тега
    // a:hover                → (0, 1, 1)   — псевдокласс = класс
    // [type="text"]          → (0, 1, 0)   — атрибут = класс
    // ::before               → (0, 0, 1)   — псевдоэлемент = тег
    // style="..."  inline    → (1, 0, 0, 0) — ещё выше
    // !important             → перебивает всё
    
    // При равной специфичности — побеждает тот, кто объявлен ПОЗЖЕ

    Современные псевдоклассы

    // :is() — сокращение для групп (специфичность = максимум из списка)
    // :is(h1, h2, h3) { margin: 0 } = h1, h2, h3 { margin: 0 }
    
    // :has() — "родительский" селектор (CSS 2023+)
    // .card:has(img) — карточки с изображением
    // form:has(:invalid) — форма с невалидным полем
    // li:has(+ li) — все элементы кроме последнего
    
    // :not() принимает список:
    // li:not(:first-child, :last-child) — не первый и не последний

    querySelector в JavaScript

    // Принимает ЛЮБОЙ валидный CSS-селектор:
    document.querySelector('#header')
    document.querySelector('.btn.active[data-type="primary"]')
    document.querySelector('form input[type="email"]:not([disabled])')
    document.querySelector('li:nth-child(3)')
    
    // querySelectorAll — NodeList (не Array, но итерируемый):
    const items = document.querySelectorAll('.card')
    [...items].map(el => el.textContent)
    Array.from(items).forEach(el => { ... })
    
    // Браузер разбирает селектор СПРАВА НАЛЕВО:
    // 'div .btn' — сначала все .btn, потом фильтрует по div-предку

    Типичные ошибки

    Ошибка 1: Чрезмерная специфичность

    /* ПЛОХО — теперь нельзя переопределить без #app */
    #app .header .nav .btn { color: red; }
    
    /* ХОРОШО — низкая специфичность, легко переопределить */
    .nav-btn { color: red; }

    Ошибка 2: Использование !important вместо правильной специфичности

    /* ПЛОХО — создаёт каскад !important, сложно поддерживать */
    .btn { color: blue !important; }
    .btn.active { color: red !important; }
    
    /* ХОРОШО — увеличиваем специфичность естественно */
    .btn { color: blue; }
    .btn.active { color: red; }  /* (0,2,0) > (0,1,0) */

    Ошибка 3: Неэффективные querySelector

    // МЕДЛЕННО — * в начале, поиск по всему документу
    document.querySelectorAll('* .btn')
    
    // БЫСТРЕЕ — ищем от конкретного контейнера
    const container = document.getElementById('app')
    container.querySelectorAll('.btn')

    В реальных проектах

  • Дебаггинг CSS: вычисление специфичности помогает найти конфликты
  • CSS Modules/BEM: уменьшают специфичность до (0,1,0) для каждого класса
  • JavaScript: element.matches('.btn.active') — проверка селектора программно
  • CSS-in-JS (styled-components): генерирует уникальные классы, обходя проблемы специфичности
  • Testing: screen.getBy... в Testing Library использует семантические селекторы
  • Примеры

    Калькулятор специфичности CSS: разбор селектора, подсчёт очков, сортировка каскада

    // Калькулятор специфичности CSS-селекторов
    
    function calculateSpecificity(selector) {
      let ids = 0, classes = 0, elements = 0
    
      let s = selector
    
      // Шаг 1: псевдоэлементы ::before, ::after (считаются как элемент)
      s = s.replace(/::[-\w]+/g, () => { elements++; return '' })
    
      // Шаг 2: псевдоклассы :hover, :nth-child() (как класс)
      s = s.replace(/:[-\w]+(?:\([^)]*\))?/g, () => { classes++; return '' })
    
      // Шаг 3: ID-селекторы #foo
      s = s.replace(/#[-\w]+/g, () => { ids++; return '' })
    
      // Шаг 4: классы .foo
      s = s.replace(/\.[-\w]+/g, () => { classes++; return '' })
    
      // Шаг 5: атрибуты [type="text"] (как класс)
      s = s.replace(/\[[^\]]*\]/g, () => { classes++; return '' })
    
      // Шаг 6: теги (оставшееся после уборки спецсимволов)
      const tags = s.replace(/[\s>+~*]/g, '').match(/[a-zA-Z][-\w]*/g) ?? []
      elements += tags.length
    
      return [ids, classes, elements]
    }
    
    function compareSpecificity(a, b) {
      for (let i = 0; i < 3; i++) {
        if (a[i] !== b[i]) return a[i] - b[i]
      }
      return 0
    }
    
    function formatSpec([ids, classes, elements]) {
      return `(${ids},${classes},${elements})`
    }
    
    function score([ids, classes, elements]) {
      return ids * 10000 + classes * 100 + elements
    }
    
    // ===== Тест набора селекторов =====
    console.log('=== Специфичность CSS-селекторов ===')
    console.log()
    
    const selectors = [
      '#header',
      '#nav .item',
      '.btn.active',
      '.btn:hover',
      'div.menu > li',
      'a',
      'div > p',
      'ul li',
      '[type="text"]',
      'input[type="email"]',
      '::before',
      'p::first-line',
      ':nth-child(2)',
      'div:not(.hidden)',
    ]
    
    for (const sel of selectors) {
      const spec = calculateSpecificity(sel)
      const s    = score(spec)
      console.log(`${sel.padEnd(26)} → ${formatSpec(spec)}  (score: ${s})`)
    }
    
    // ===== Каскад: сортировка по специфичности =====
    console.log('\n=== CSS Каскад: от наименее к наиболее специфичному ===')
    
    const cssRules = [
      { selector: 'a',               value: 'color: blue' },
      { selector: '.link',           value: 'color: green' },
      { selector: 'nav .link',       value: 'color: teal' },
      { selector: '#header a',       value: 'color: red' },
      { selector: '.nav #main-link', value: 'color: orange' },
    ]
    
    const sorted = cssRules
      .map(r => ({ ...r, spec: calculateSpecificity(r.selector) }))
      .sort((a, b) => compareSpecificity(a.spec, b.spec))
    
    for (const rule of sorted) {
      console.log(`  ${formatSpec(rule.spec).padEnd(12)} ${rule.selector.padEnd(20)} → ${rule.value}`)
    }
    
    const winner = sorted[sorted.length - 1]
    console.log(`\nПобеждает: "${winner.value}" (наибольшая специфичность: ${formatSpec(winner.spec)})`)
    
    // ===== Сравнение пар =====
    console.log('\n=== Сравнение пар (какой стиль победит?) ===')
    const pairs = [
      ['#nav a', '.nav-link:hover'],
      ['.btn.primary', 'div .btn'],
      ['li:nth-child(2)', '.list-item'],
      ['p::before', '.pseudo'],
    ]
    
    for (const [a, b] of pairs) {
      const specA = calculateSpecificity(a)
      const specB = calculateSpecificity(b)
      const cmp   = compareSpecificity(specA, specB)
      const winner = cmp > 0 ? a : cmp < 0 ? b : 'ничья (порядок решает)'
      console.log(`  "${a}" vs "${b}"`)
      console.log(`  ${formatSpec(specA)} vs ${formatSpec(specB)} → побеждает: ${winner}`)
    }

    CSS селекторы и специфичность

    Ты пишешь стиль .btn { color: red }, но кнопка по-прежнему синяя — потому что где-то есть #header .btn { color: blue } с большей специфичностью. Или в JavaScript: document.querySelector('form input:not([disabled]):first-child') — ты должен понимать что это вернёт. Специфичность и селекторы — это «язык приоритетов» в CSS.

    Какую проблему решает

    В больших проектах CSS-стили конфликтуют. Специфичность — правило, по которому браузер решает какой стиль применить. JavaScript использует CSS-селекторы для поиска элементов — чем лучше ты знаешь синтаксис, тем точнее запросы.

    На основе предыдущих уроков

  • DOM: document.querySelector, querySelectorAll — принимают CSS-селекторы
  • RegExp: вычисление специфичности требует разбора строк
  • CSS: без понимания специфичности невозможно дебажить CSS
  • Типы селекторов

    // Базовые:
    // div            — по тегу
    // .foo           — по классу
    // #foo           — по ID
    // *              — все элементы
    
    // Атрибуты:
    // [type="text"]        — точное значение
    // [href^="https"]      — начинается с "https"
    // [src$=".png"]        — заканчивается на ".png"
    // [class*="btn"]       — содержит "btn"
    // [data-active]        — атрибут существует
    
    // Псевдоклассы:
    // :hover :focus :active :checked :disabled
    // :first-child :last-child :nth-child(2n+1)
    // :not(.hidden) :is(h1, h2, h3) :has(img)
    
    // Псевдоэлементы:
    // ::before ::after ::placeholder ::selection
    
    // Комбинаторы:
    // div p          — потомок (любой уровень)
    // div > p        — прямой дочерний
    // div + p        — следующий сосед
    // div ~ p        — все следующие соседи

    Специфичность — как браузер считает приоритет

    Специфичность записывается как тройка (id, class, element):

    // #header                → (1, 0, 0)   — выигрывает почти всегда
    // .btn.active            → (0, 2, 0)   — два класса
    // div.menu > li          → (0, 1, 2)   — класс + два тега
    // a:hover                → (0, 1, 1)   — псевдокласс = класс
    // [type="text"]          → (0, 1, 0)   — атрибут = класс
    // ::before               → (0, 0, 1)   — псевдоэлемент = тег
    // style="..."  inline    → (1, 0, 0, 0) — ещё выше
    // !important             → перебивает всё
    
    // При равной специфичности — побеждает тот, кто объявлен ПОЗЖЕ

    Современные псевдоклассы

    // :is() — сокращение для групп (специфичность = максимум из списка)
    // :is(h1, h2, h3) { margin: 0 } = h1, h2, h3 { margin: 0 }
    
    // :has() — "родительский" селектор (CSS 2023+)
    // .card:has(img) — карточки с изображением
    // form:has(:invalid) — форма с невалидным полем
    // li:has(+ li) — все элементы кроме последнего
    
    // :not() принимает список:
    // li:not(:first-child, :last-child) — не первый и не последний

    querySelector в JavaScript

    // Принимает ЛЮБОЙ валидный CSS-селектор:
    document.querySelector('#header')
    document.querySelector('.btn.active[data-type="primary"]')
    document.querySelector('form input[type="email"]:not([disabled])')
    document.querySelector('li:nth-child(3)')
    
    // querySelectorAll — NodeList (не Array, но итерируемый):
    const items = document.querySelectorAll('.card')
    [...items].map(el => el.textContent)
    Array.from(items).forEach(el => { ... })
    
    // Браузер разбирает селектор СПРАВА НАЛЕВО:
    // 'div .btn' — сначала все .btn, потом фильтрует по div-предку

    Типичные ошибки

    Ошибка 1: Чрезмерная специфичность

    /* ПЛОХО — теперь нельзя переопределить без #app */
    #app .header .nav .btn { color: red; }
    
    /* ХОРОШО — низкая специфичность, легко переопределить */
    .nav-btn { color: red; }

    Ошибка 2: Использование !important вместо правильной специфичности

    /* ПЛОХО — создаёт каскад !important, сложно поддерживать */
    .btn { color: blue !important; }
    .btn.active { color: red !important; }
    
    /* ХОРОШО — увеличиваем специфичность естественно */
    .btn { color: blue; }
    .btn.active { color: red; }  /* (0,2,0) > (0,1,0) */

    Ошибка 3: Неэффективные querySelector

    // МЕДЛЕННО — * в начале, поиск по всему документу
    document.querySelectorAll('* .btn')
    
    // БЫСТРЕЕ — ищем от конкретного контейнера
    const container = document.getElementById('app')
    container.querySelectorAll('.btn')

    В реальных проектах

  • Дебаггинг CSS: вычисление специфичности помогает найти конфликты
  • CSS Modules/BEM: уменьшают специфичность до (0,1,0) для каждого класса
  • JavaScript: element.matches('.btn.active') — проверка селектора программно
  • CSS-in-JS (styled-components): генерирует уникальные классы, обходя проблемы специфичности
  • Testing: screen.getBy... в Testing Library использует семантические селекторы
  • Примеры

    Калькулятор специфичности CSS: разбор селектора, подсчёт очков, сортировка каскада

    // Калькулятор специфичности CSS-селекторов
    
    function calculateSpecificity(selector) {
      let ids = 0, classes = 0, elements = 0
    
      let s = selector
    
      // Шаг 1: псевдоэлементы ::before, ::after (считаются как элемент)
      s = s.replace(/::[-\w]+/g, () => { elements++; return '' })
    
      // Шаг 2: псевдоклассы :hover, :nth-child() (как класс)
      s = s.replace(/:[-\w]+(?:\([^)]*\))?/g, () => { classes++; return '' })
    
      // Шаг 3: ID-селекторы #foo
      s = s.replace(/#[-\w]+/g, () => { ids++; return '' })
    
      // Шаг 4: классы .foo
      s = s.replace(/\.[-\w]+/g, () => { classes++; return '' })
    
      // Шаг 5: атрибуты [type="text"] (как класс)
      s = s.replace(/\[[^\]]*\]/g, () => { classes++; return '' })
    
      // Шаг 6: теги (оставшееся после уборки спецсимволов)
      const tags = s.replace(/[\s>+~*]/g, '').match(/[a-zA-Z][-\w]*/g) ?? []
      elements += tags.length
    
      return [ids, classes, elements]
    }
    
    function compareSpecificity(a, b) {
      for (let i = 0; i < 3; i++) {
        if (a[i] !== b[i]) return a[i] - b[i]
      }
      return 0
    }
    
    function formatSpec([ids, classes, elements]) {
      return `(${ids},${classes},${elements})`
    }
    
    function score([ids, classes, elements]) {
      return ids * 10000 + classes * 100 + elements
    }
    
    // ===== Тест набора селекторов =====
    console.log('=== Специфичность CSS-селекторов ===')
    console.log()
    
    const selectors = [
      '#header',
      '#nav .item',
      '.btn.active',
      '.btn:hover',
      'div.menu > li',
      'a',
      'div > p',
      'ul li',
      '[type="text"]',
      'input[type="email"]',
      '::before',
      'p::first-line',
      ':nth-child(2)',
      'div:not(.hidden)',
    ]
    
    for (const sel of selectors) {
      const spec = calculateSpecificity(sel)
      const s    = score(spec)
      console.log(`${sel.padEnd(26)} → ${formatSpec(spec)}  (score: ${s})`)
    }
    
    // ===== Каскад: сортировка по специфичности =====
    console.log('\n=== CSS Каскад: от наименее к наиболее специфичному ===')
    
    const cssRules = [
      { selector: 'a',               value: 'color: blue' },
      { selector: '.link',           value: 'color: green' },
      { selector: 'nav .link',       value: 'color: teal' },
      { selector: '#header a',       value: 'color: red' },
      { selector: '.nav #main-link', value: 'color: orange' },
    ]
    
    const sorted = cssRules
      .map(r => ({ ...r, spec: calculateSpecificity(r.selector) }))
      .sort((a, b) => compareSpecificity(a.spec, b.spec))
    
    for (const rule of sorted) {
      console.log(`  ${formatSpec(rule.spec).padEnd(12)} ${rule.selector.padEnd(20)} → ${rule.value}`)
    }
    
    const winner = sorted[sorted.length - 1]
    console.log(`\nПобеждает: "${winner.value}" (наибольшая специфичность: ${formatSpec(winner.spec)})`)
    
    // ===== Сравнение пар =====
    console.log('\n=== Сравнение пар (какой стиль победит?) ===')
    const pairs = [
      ['#nav a', '.nav-link:hover'],
      ['.btn.primary', 'div .btn'],
      ['li:nth-child(2)', '.list-item'],
      ['p::before', '.pseudo'],
    ]
    
    for (const [a, b] of pairs) {
      const specA = calculateSpecificity(a)
      const specB = calculateSpecificity(b)
      const cmp   = compareSpecificity(specA, specB)
      const winner = cmp > 0 ? a : cmp < 0 ? b : 'ничья (порядок решает)'
      console.log(`  "${a}" vs "${b}"`)
      console.log(`  ${formatSpec(specA)} vs ${formatSpec(specB)} → побеждает: ${winner}`)
    }

    Задание

    Реализуй функцию `calculateSpecificity(selector)`, которая принимает CSS-селектор и возвращает его специфичность в виде массива `[ids, classes, elements]`. Алгоритм — последовательная замена компонентов с инкрементом счётчиков: псевдоэлементы (elements++), псевдоклассы (classes++), ID (ids++), классы (classes++), атрибуты (classes++), теги (elements++).

    Подсказка

    Каждый шаг: s = s.replace(regex, () => { counter++; return "" }). В конце elements += tags.length. Возвращай [ids, classes, elements]. Порядок шагов важен: псевдоэлементы (::) до псевдоклассов (:)

    Загружаем среду выполнения...
    Загружаем AI-помощника...