Estilo baseado no estado do filho

Recursos da Web: :has() :not()

Historicamente, os seletores de CSS só podiam ser usados de cima para baixo. Era possível estilizar um elemento filho com base no pai, mas não o contrário. A pseudoclasse :has() muda isso, permitindo que você estilize condicionalmente um elemento de contêiner dependendo da presença ou do estado dos descendentes dele.

Ao combinar :has() com pseudoclasses de estado (como :checked, :focus, :valid, :invalid ou :not()), é possível criar componentes de interface complexos e interativos inteiramente em CSS, sem precisar de JavaScript para alternar classes "modificadoras" (como .is-active ou .has-error) em elementos pai.

Isso é útil para componentes que precisam responder a interações internas, como um botão de tema localizado que reage a uma caixa de seleção (:checked), um grupo de formulários que destaca um erro (:invalid) ou um card que se eleva quando um link filho está em foco (:focus-within).

Como implementar o estilo de contêiner com base no estado

OBRIGATÓRIO: use o seletor :has() no elemento de contêiner para detectar o estado específico (por exemplo, :checked, :focus, :invalid) do elemento filho interativo.

Para criar um componente que muda o estilo com base no estado de um filho:

  1. Defina o estilo padrão: defina variáveis CSS no contêiner para definir o estado base.
  2. Aplicar substituições com base no estado: segmente o contêiner com :has([child-selector]:[state]) e redefina as variáveis CSS para o estado ativo ou alternativo.

Exemplo: um componente que muda de tema com base em uma alternância filha.

/* 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>

Dica de performance: ao usar :has(), defina o escopo do seletor para o contêiner mais específico possível (como .theme-card). Evite ancorá-lo a elementos de nível muito alto, como body:has(...), se as mudanças de estilo forem localizadas, já que consultas :has() amplas podem acionar mais recálculos de layout.

Estratégias de fallback

Se o estilo baseado em estado for essencial para a experiência do usuário ou o layout da página, forneça um substituto para navegadores que não oferecem suporte ao seletor :has(). Para efeitos puramente decorativos, :has() pode ser usado como um aprimoramento progressivo sem substituto.

OBRIGATÓRIO: ao implementar um substituto para recursos críticos, use @supports not selector(:has(*)) no CSS para definir um substituto tradicional baseado em classe. Se a mudança de estado crítico depender da interação do usuário, você também precisará usar um pequeno script inline com CSS.supports() para alternar essa classe com base no evento JavaScript equivalente (por exemplo, change, focus, blur) que representa a mudança de estado.

/* 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);
    });
  }
}