Стилизация на основе состояния дочернего элемента

Веб-функции: :has() :not()

Исторически селекторы CSS могли перемещаться только вниз – вы могли стилизовать дочерний элемент на основе его родительского элемента, но не родительский элемент на основе его дочернего элемента. Псевдокласс :has() позволяет условно стилизовать элемент контейнера в зависимости от наличия или состояния его потомков.

Комбинируя :has() с псевдоклассами состояния (например, :checked, :focus, :valid, :invalid или :not()), вы можете создавать сложные интерактивные компоненты пользовательского интерфейса полностью на CSS, без необходимости использовать JavaScript для переключения классов-модификаторов (например, .is-active или .has-error) в родительских элементах.

Это особенно полезно для компонентов, которые должны реагировать на внутренние взаимодействия, например локализованный переключатель темы, реагирующий на флажок (:checked), группа форм, выделяющая ошибку (:invalid), или карточка, поднимающаяся, когда дочерняя ссылка находится в фокусе (:focus-within).

Как реализовать стили контейнера на основе состояния

ОБЯЗАТЕЛЬНО. Вы должны использовать селектор :has() в элементе контейнера, чтобы определять конкретное состояние (например, :checked, :focus, :invalid) его интерактивного дочернего элемента.

Чтобы создать компонент, стиль которого меняется в зависимости от состояния дочернего элемента, выполните следующие действия:

  1. Определите стиль по умолчанию. Задайте переменные CSS для контейнера, чтобы определить его базовое состояние.
  2. Примените переопределения на основе состояния. Настройте таргетинг на контейнер с помощью :has([child-selector]:[state]) и переопределите переменные CSS для активного или альтернативного состояния.

Пример: компонент, который меняет тему в зависимости от дочернего переключателя.

/* 1. Define the default state on the component container */
.theme-card {
  /* Using custom properties makes state-switching cleaner */
  --card-bg: #ffffff;
  --card-text: #333333;
  --card-border: #cccccc;

  background-color: var(--card-bg);
  color: var(--card-text);
  border: 1px solid var(--card-border);

  /* Use a transition for smooth state changes */
  transition: background-color 0.3s, color 0.3s;
}

/* 2. Apply styles when the child enters the specific state */
/* MANDATORY: Target the container and use :has() to check the descendant's state */
.theme-card:has(.theme-toggle:checked) {
  /* Override the properties for the active or alternate state */
  --card-bg: #222222;
  --card-text: #f0f0f0;
  --card-border: #555555;
}
/* You can also combine :has() and :not() to target specific negative states */
/* This selects a card that DOES NOT have a toggle in the checked state */
.theme-card:not(:has(.theme-toggle:checked)) {
  /* Optional: Explicit styles for the unchecked state if needed */
}
<!-- The container element that receives the styling -->
<div class="theme-card">
  <!-- The child element whose state controls the parent -->
  <label>
    <input type="checkbox" class="theme-toggle">
    Enable Dark Mode
  </label>

  <h2>Card Title</h2>
  <p>The style of this entire card is controlled by the checkbox above.</p>
</div>

Совет по повышению производительности. При использовании :has() ограничьте селектор наиболее конкретным контейнером (например, .theme-card). Не привязывайте его к элементам очень высокого уровня, таким как body:has(...), если изменения стиля локализованы, поскольку широкие запросы :has() могут привести к дополнительным перерасчетам макета.

Резервные стратегии

Если стили, основанные на состоянии, критически важны для удобства пользователей или макета страницы, необходимо предоставить резервный вариант для браузеров, которые не поддерживают селектор :has(). Для чисто декоративных эффектов :has() можно использовать как прогрессивное улучшение без резервного варианта.

ОБЯЗАТЕЛЬНО. При реализации резервного варианта для критически важных функций вы должны использовать @supports not selector(:has(*)) в CSS, чтобы определить традиционный резервный вариант на основе классов. Если критическое изменение состояния зависит от взаимодействия с пользователем, вы также должны использовать небольшой встроенный скрипт с CSS.supports(), чтобы переключать этот класс на основе эквивалентного события JavaScript (например, change, focus, blur), представляющего изменение состояния.

/* Fallback CSS for older browsers */
/* We check if the browser DOES NOT support the :has() selector */
@supports not selector(:has(*)) {
  /* Define a traditional modifier class that applies the exact same overrides */
  .theme-card.is-active {
    --card-bg: #222222;
    --card-text: #f0f0f0;
    --card-border: #555555;
  }
}
/* Fallback JavaScript for older browsers */
/* Check for support before running the script to avoid unnecessary work in modern browsers */
if (!CSS.supports('selector(:has(*))')) {
  const toggle = document.querySelector('.theme-toggle');
  const card = document.querySelector('.theme-card');

  if (toggle && card) {
    // Manually toggle the fallback class when the input state changes
    toggle.addEventListener('change', (e) => {
      card.classList.toggle('is-active', e.target.checked);
    });
  }
}