Históricamente, los selectores CSS solo podían recorrer hacia abajo: podías aplicar diseño a un elemento secundario en función de su elemento superior, pero no a un elemento superior en función de su elemento secundario. La pseudoclase :has() cambia esto, lo que te permite aplicar diseño de forma condicional a un elemento contenedor según la presencia o el estado de sus descendientes.
Si combinas :has() con seudoclases de estado (como :checked, :focus, :valid, :invalid o :not()), puedes compilar componentes de IU interactivos y complejos por completo en CSS, sin necesidad de JavaScript para activar o desactivar clases de "modificador" (como .is-active o .has-error) en los elementos superiores.
Esto es particularmente útil para los componentes que necesitan responder a interacciones internas, como un botón de activación de tema localizado que reacciona a una casilla de verificación (:checked), un grupo de formularios que destaca un error (:invalid) o una tarjeta que se eleva cuando se enfoca un vínculo secundario (:focus-within).
Cómo implementar un diseño de contenedor basado en el estado
OBLIGATORIO: Debes usar el selector :has() en el elemento contenedor para detectar el estado específico (p.ej., :checked, :focus, :invalid) de su elemento secundario interactivo.
Para compilar un componente que cambie su estilo según el estado de un elemento secundario, sigue estos pasos:
- Define el diseño predeterminado: Establece variables de CSS en el contenedor para definir su estado base.
- Aplica anulaciones basadas en el estado: Segmenta el contenedor con
:has([child-selector]:[state])y redefine las variables de CSS para el estado activo o alternativo.
Ejemplo: Un componente que cambia de tema según un botón de activación secundario.
/* 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>
Sugerencia de rendimiento: Cuando uses :has(), limita el selector al contenedor más específico posible (como .theme-card). Evita fijarlo a elementos de nivel muy alto, como body:has(...), si los cambios de estilo están localizados, ya que las consultas :has() amplias pueden activar más recálculos de diseño.
Estrategias de resguardo
Si el diseño basado en el estado es fundamental para la experiencia del usuario o el diseño de la página, debes proporcionar un resguardo para los navegadores que no admiten el selector :has(). Para efectos puramente decorativos, se puede usar :has() como una mejora progresiva sin un resguardo.
OBLIGATORIO: Cuando implementes un resguardo para funciones fundamentales, debes usar @supports not selector(:has(*)) en tu CSS para definir un resguardo tradicional basado en clases. Si el cambio de estado crítico depende de la interacción del usuario, también debes usar una pequeña secuencia de comandos intercalada con CSS.supports() para activar o desactivar esa clase en función del evento de JavaScript equivalente (p.ej., change, focus, blur) que representa el cambio 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);
});
}
}