← HTML & CSS/:has(), :is(), :where() — продвинутые CSS-селекторы#43 из 383← ПредыдущийСледующий →+15 XP
Полезно по теме:Гайд: старт в frontendПрактика: DOM и событияТермин: DOMМаршрут: старт с нуля

:has(), :is(), :where() — продвинутые CSS-селекторы

Эти три псевдокласса появились в браузерах в 2022–2023 годах и изменили возможности CSS-селекторов. :has() — первый настоящий «родительский» селектор в истории CSS.

:has() — родительский селектор

До :has() CSS мог выбирать только потомков, но не родителей. Теперь можно:

/* Карточка, которая содержит изображение */
.card:has(img) {
  padding: 0;              /* Убираем отступ если есть картинка */
}

/* Форма с невалидным полем */
form:has(input:invalid) {
  border: 2px solid #ef4444;
}

/* Статья с более чем одним абзацем */
article:has(p ~ p) {
  columns: 2;              /* Двухколоночный макет */
}

/* Навигация без логотипа */
.header:not(:has(.logo)) {
  justify-content: center;
}

/* Параграф перед изображением */
p:has(+ img) {
  margin-bottom: 4px;      /* Уменьшаем отступ перед картинкой */
}

:is() — сокращённый список селекторов

Позволяет избежать повторений в длинных селекторах:

/* Без :is() — длинно */
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
  color: inherit;
}

/* С :is() — компактно */
:is(h1, h2, h3, h4, h5, h6) a {
  color: inherit;
}

/* Вложение с :is() */
.card :is(h2, h3) {
  font-size: 1.2em;
}

/* «Прощающий» список — если один селектор невалиден, остальные работают */
:is(.card, .article, :unknown-pseudo) p {
  margin: 0;  /* Сработает для .card и .article, :unknown-pseudo проигнорируется */
}

Специфичность :is() = специфичности самого «тяжёлого» аргумента.

:where() — нулевая специфичность

Как :is(), но с нулевой специфичностью. Идеально для сброса стилей и базовых стилей, которые легко переопределить:

/* Специфичность :is(h1, .title) = 0-1-0 (от .title) */
:is(h1, .title) { color: black; }

/* Специфичность :where(h1, .title) = 0-0-0 */
:where(h1, .title) { color: black; }   /* Легко переопределить любым стилем */

/* Применение: базовые стили фреймворка */
:where(h1, h2, h3, h4, h5, h6) {
  font-weight: bold;
  line-height: 1.2;
  /* Нулевая специфичность — пользователь легко переопределит */
}

Улучшенный :not()

Теперь :not() принимает список:

/* Старый :not() — только один аргумент */
a:not(.disabled):not(.hidden) { }

/* Новый :not() — список */
a:not(.disabled, .hidden, [aria-hidden]) { }

/* Все абзацы не в aside и не в footer */
p:not(aside p, footer p) {
  max-width: 65ch;
}

Реальные примеры

/* Тёмная тема для всего сайта */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #1a202c;
    --color-text: #f7fafc;
  }
}

/* Форма с обязательными полями */
form:has(input[required]:placeholder-shown) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* Навигационный элемент с активной страницей */
.nav-item:has([aria-current="page"]) {
  background: #ede9fe;
  font-weight: bold;
}

Примеры

Реализация матчинга :has(), :is(), :where() на виртуальном DOM-дереве

// Симуляция CSS-селекторов :has(), :is(), :where() на JS-структурах
const dom = [
  { id: 1, tag: 'div', classes: ['card'], parent: null, children: [2, 3] },
  { id: 2, tag: 'img', classes: [],       parent: 1,    children: [] },
  { id: 3, tag: 'p',   classes: ['text'], parent: 1,    children: [] },
  { id: 4, tag: 'div', classes: ['card'], parent: null, children: [5] },
  { id: 5, tag: 'p',   classes: ['text'], parent: 4,    children: [] },
  { id: 6, tag: 'section', classes: [],   parent: null, children: [7, 8] },
  { id: 7, tag: 'h2', classes: ['title'], parent: 6,    children: [] },
  { id: 8, tag: 'p',  classes: [],        parent: 6,    children: [] },
]

const byId = Object.fromEntries(dom.map(el => [el.id, el]))

// Базовый матчинг: ".class" или "tag"
function matchesSimple(el, selector) {
  if (selector.startsWith('.')) return el.classes.includes(selector.slice(1))
  return el.tag === selector
}

// :has(child) — родитель с таким потомком
function hasDescendant(el, childSelector) {
  return el.children.some(childId => {
    const child = byId[childId]
    return matchesSimple(child, childSelector) || hasDescendant(child, childSelector)
  })
}

// :is(...selectors) — хотя бы один из селекторов
function matchesIs(el, selectors) {
  return selectors.some(sel => matchesSimple(el, sel))
}

// Применение
console.log('=== :has() ===')
const cardsWithImg = dom.filter(el =>
  matchesSimple(el, '.card') && hasDescendant(el, 'img')
)
console.log('div.card:has(img) IDs:', cardsWithImg.map(e => e.id))  // [1] — только первая карточка

const cardsWithP = dom.filter(el =>
  matchesSimple(el, '.card') && hasDescendant(el, 'p')
)
console.log('div.card:has(p) IDs:', cardsWithP.map(e => e.id))  // [1, 4]

console.log('\n=== :is() ===')
// :is(h2, h3, h4) p — параграфы внутри заголовочных элементов... (или элементы рядом)
const headings = dom.filter(el => matchesIs(el, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']))
console.log(':is(h1,h2,h3,h4) IDs:', headings.map(e => e.id))  // [7]

console.log('\n=== :where() (та же логика, нулевая специфичность) ===')
// :where(div, section) — все div или section
const containers = dom.filter(el => matchesIs(el, ['div', 'section']))
console.log(':where(div, section) IDs:', containers.map(e => e.id))  // [1, 4, 6]

console.log('\n=== Специфичность (концептуально) ===')
console.log(':is(.card, div) — специфичность 0-1-0 (от .card)')
console.log(':where(.card, div) — специфичность 0-0-0')
console.log(':has(img) — специфичность 0-0-0 (как :is)')

:has(), :is(), :where() — продвинутые CSS-селекторы

Эти три псевдокласса появились в браузерах в 2022–2023 годах и изменили возможности CSS-селекторов. :has() — первый настоящий «родительский» селектор в истории CSS.

:has() — родительский селектор

До :has() CSS мог выбирать только потомков, но не родителей. Теперь можно:

/* Карточка, которая содержит изображение */
.card:has(img) {
  padding: 0;              /* Убираем отступ если есть картинка */
}

/* Форма с невалидным полем */
form:has(input:invalid) {
  border: 2px solid #ef4444;
}

/* Статья с более чем одним абзацем */
article:has(p ~ p) {
  columns: 2;              /* Двухколоночный макет */
}

/* Навигация без логотипа */
.header:not(:has(.logo)) {
  justify-content: center;
}

/* Параграф перед изображением */
p:has(+ img) {
  margin-bottom: 4px;      /* Уменьшаем отступ перед картинкой */
}

:is() — сокращённый список селекторов

Позволяет избежать повторений в длинных селекторах:

/* Без :is() — длинно */
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
  color: inherit;
}

/* С :is() — компактно */
:is(h1, h2, h3, h4, h5, h6) a {
  color: inherit;
}

/* Вложение с :is() */
.card :is(h2, h3) {
  font-size: 1.2em;
}

/* «Прощающий» список — если один селектор невалиден, остальные работают */
:is(.card, .article, :unknown-pseudo) p {
  margin: 0;  /* Сработает для .card и .article, :unknown-pseudo проигнорируется */
}

Специфичность :is() = специфичности самого «тяжёлого» аргумента.

:where() — нулевая специфичность

Как :is(), но с нулевой специфичностью. Идеально для сброса стилей и базовых стилей, которые легко переопределить:

/* Специфичность :is(h1, .title) = 0-1-0 (от .title) */
:is(h1, .title) { color: black; }

/* Специфичность :where(h1, .title) = 0-0-0 */
:where(h1, .title) { color: black; }   /* Легко переопределить любым стилем */

/* Применение: базовые стили фреймворка */
:where(h1, h2, h3, h4, h5, h6) {
  font-weight: bold;
  line-height: 1.2;
  /* Нулевая специфичность — пользователь легко переопределит */
}

Улучшенный :not()

Теперь :not() принимает список:

/* Старый :not() — только один аргумент */
a:not(.disabled):not(.hidden) { }

/* Новый :not() — список */
a:not(.disabled, .hidden, [aria-hidden]) { }

/* Все абзацы не в aside и не в footer */
p:not(aside p, footer p) {
  max-width: 65ch;
}

Реальные примеры

/* Тёмная тема для всего сайта */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #1a202c;
    --color-text: #f7fafc;
  }
}

/* Форма с обязательными полями */
form:has(input[required]:placeholder-shown) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* Навигационный элемент с активной страницей */
.nav-item:has([aria-current="page"]) {
  background: #ede9fe;
  font-weight: bold;
}

Примеры

Реализация матчинга :has(), :is(), :where() на виртуальном DOM-дереве

// Симуляция CSS-селекторов :has(), :is(), :where() на JS-структурах
const dom = [
  { id: 1, tag: 'div', classes: ['card'], parent: null, children: [2, 3] },
  { id: 2, tag: 'img', classes: [],       parent: 1,    children: [] },
  { id: 3, tag: 'p',   classes: ['text'], parent: 1,    children: [] },
  { id: 4, tag: 'div', classes: ['card'], parent: null, children: [5] },
  { id: 5, tag: 'p',   classes: ['text'], parent: 4,    children: [] },
  { id: 6, tag: 'section', classes: [],   parent: null, children: [7, 8] },
  { id: 7, tag: 'h2', classes: ['title'], parent: 6,    children: [] },
  { id: 8, tag: 'p',  classes: [],        parent: 6,    children: [] },
]

const byId = Object.fromEntries(dom.map(el => [el.id, el]))

// Базовый матчинг: ".class" или "tag"
function matchesSimple(el, selector) {
  if (selector.startsWith('.')) return el.classes.includes(selector.slice(1))
  return el.tag === selector
}

// :has(child) — родитель с таким потомком
function hasDescendant(el, childSelector) {
  return el.children.some(childId => {
    const child = byId[childId]
    return matchesSimple(child, childSelector) || hasDescendant(child, childSelector)
  })
}

// :is(...selectors) — хотя бы один из селекторов
function matchesIs(el, selectors) {
  return selectors.some(sel => matchesSimple(el, sel))
}

// Применение
console.log('=== :has() ===')
const cardsWithImg = dom.filter(el =>
  matchesSimple(el, '.card') && hasDescendant(el, 'img')
)
console.log('div.card:has(img) IDs:', cardsWithImg.map(e => e.id))  // [1] — только первая карточка

const cardsWithP = dom.filter(el =>
  matchesSimple(el, '.card') && hasDescendant(el, 'p')
)
console.log('div.card:has(p) IDs:', cardsWithP.map(e => e.id))  // [1, 4]

console.log('\n=== :is() ===')
// :is(h2, h3, h4) p — параграфы внутри заголовочных элементов... (или элементы рядом)
const headings = dom.filter(el => matchesIs(el, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']))
console.log(':is(h1,h2,h3,h4) IDs:', headings.map(e => e.id))  // [7]

console.log('\n=== :where() (та же логика, нулевая специфичность) ===')
// :where(div, section) — все div или section
const containers = dom.filter(el => matchesIs(el, ['div', 'section']))
console.log(':where(div, section) IDs:', containers.map(e => e.id))  // [1, 4, 6]

console.log('\n=== Специфичность (концептуально) ===')
console.log(':is(.card, div) — специфичность 0-1-0 (от .card)')
console.log(':where(.card, div) — специфичность 0-0-0')
console.log(':has(img) — специфичность 0-0-0 (как :is)')

Задание

Создай форму с умной валидацией через `:has()`, `:is()` и `:where()`. Используй `:has(input:invalid)` чтобы выделить форму красной рамкой при невалидном вводе. Применяй `:is(h2, h3)` для заголовков карточки. Используй `:where(p, li)` для базовых стилей текста с нулевой специфичностью.

Подсказка

`:has(input:invalid:not(:placeholder-shown))` — форма с невалидным заполненным полем: `border-color: #e53e3e`. `input:invalid` — `border-color: #e53e3e`. `input:valid` — `border-color: #38a169`. `:is(h2, h3)` — `color: #7b2ff7`. `:where(p, li)` — `color: #555`, `font-size: 14px`. `.card:has(img)` — `padding-top: 0`.

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