Эти три псевдокласса появились в браузерах в 2022–2023 годах и изменили возможности CSS-селекторов. :has() — первый настоящий «родительский» селектор в истории CSS.
До :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() — длинно */
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() = специфичности самого «тяжёлого» аргумента.
Как :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() — только один аргумент */
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)')Эти три псевдокласса появились в браузерах в 2022–2023 годах и изменили возможности CSS-селекторов. :has() — первый настоящий «родительский» селектор в истории CSS.
До :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() — длинно */
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() = специфичности самого «тяжёлого» аргумента.
Как :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() — только один аргумент */
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`.