El patrón de variante: modos que se propagan
Los tres usos que ya funcionan hoy: superficies invertidas, densidad y estado calculado con :has(). Y el criterio para elegir entre consulta de estilo, atributo y selector.
Una consulta de estilo es útil exactamente cuando un ancestro necesita comunicar una decisión a descendientes que no conoce. Eso descarta la mayoría de los casos donde la gente la usa por curiosidad y deja tres patrones que aportan algo real: superficies que invierten, densidad que se propaga y estado calculado con :has() que viaja hacia abajo. Los tres se pueden escribir hoy y los tres degradan a algo aceptable donde no hay soporte.
- Implementar un sistema de superficies donde los componentes se adaptan sin conocer el tema.
- Propagar una decisión de densidad a través de varios niveles de anidamiento.
- Combinar
:has()para calcular un estado con una consulta de estilo para consumirlo. - Elegir entre consulta de estilo, atributo y selector descendente con un criterio claro.
Los tres patrones que funcionan hoy
Patrón 1: superficies
El caso canónico. Una sección declara sobre qué tipo de superficie está pintando, y todos los componentes de dentro —a cualquier profundidad, escritos por cualquiera— se adaptan.
/* El contrato del sistema: una sola propiedad */
.superficie-clara { --superficie: clara; background: oklch(0.98 0.01 250); color: oklch(0.2 0.02 250); }
.superficie-oscura { --superficie: oscura; background: oklch(0.22 0.03 265); color: oklch(0.95 0.01 265); }
/* Los componentes reaccionan sin saber quien decidio */
@container style(--superficie: oscura) {
.boton-secundario { border-color: color-mix(in oklch, currentColor 45%, transparent); }
.separador { border-color: color-mix(in oklch, currentColor 25%, transparent); }
.codigo { background: color-mix(in oklch, black 25%, transparent); }
.sombra { box-shadow: 0 1px 3px rgb(0 0 0 / 0.5); }
}
@container style(--superficie: clara) {
.sombra { box-shadow: 0 1px 3px rgb(0 0 0 / 0.12); }
}
Lo que hace valioso a este patrón es lo que no contiene: ningún selector menciona .superficie-oscura. El botón secundario no sabe que existe un sistema de temas; solo sabe leer una propiedad. Puedes añadir una tercera superficie mañana sin tocar ninguno de los componentes que ya reaccionan.
Esto no sustituye a prefers-color-scheme ni a un sistema de tokens de tema, que operan a nivel de documento. Resuelve el caso distinto de tener varias superficies simultáneas en la misma página: una portada clara con una banda oscura en medio, un panel invertido, una tarjeta destacada sobre fondo de color.
Patrón 2: densidad
La misma mecánica aplicada a una decisión de compacidad que atraviesa varios niveles:
.tabla-densa { --densidad: compacta; }
@container style(--densidad: compacta) {
.celda { padding-block: 0.25rem; }
.avatar { inline-size: 1.5rem; }
.badge { font-size: 0.75rem; padding-inline: 0.375rem; }
.icono-fila { display: none; }
}
Aquí la alternativa con selectores existe y es corta: .tabla-densa .celda. La consulta de estilo gana cuando el número de componentes afectados crece y no todos están bajo el mismo contenedor, o cuando la densidad la puede fijar tanto una tabla como un panel como una preferencia del usuario aplicada en la raíz. Con selectores acabarías con listas de ancestros; con una propiedad heredada, el origen deja de importar.
Patrón 3: estado calculado con has
Este es el más interesante porque compone dos características modernas. :has() puede calcular un estado mirando el contenido; una custom property puede transportarlo hacia abajo; una consulta de estilo puede consumirlo a cualquier profundidad.
/* 1. Calcular: el formulario detecta que algo es invalido */
.formulario:has(:user-invalid) { --estado: error; }
.formulario:has([aria-busy="true"]) { --estado: cargando; }
/* 2. Consumir: cualquier descendiente reacciona sin saber quien lo detecto */
@container style(--estado: error) {
.barra-envio { border-block-start-color: oklch(0.62 0.22 25); }
.contador { color: oklch(0.62 0.22 25); }
}
@container style(--estado: cargando) {
.barra-envio button { pointer-events: none; opacity: 0.6; }
}
Con selectores puros, cada descendiente tendría que repetir el :has() completo del formulario, y esa repetición es cara de mantener y cara de evaluar. Con este esquema, la condición se evalúa una vez y el resultado se propaga.
Este patrón es de presentación. El estado de error debe estar además expresado de forma que la accesibilidad lo vea: aria-invalid en el campo, un mensaje asociado con aria-describedby, un role="alert" en el resumen. Una consulta de estilo pinta; no comunica nada a un lector de pantalla.
Cuándo NO usar una consulta de estilo
Tres casos donde hay algo mejor.
El elemento se estila a sí mismo. Una tarjeta que debe cambiar su propio borde según su variante no puede usar una consulta de estilo, porque no se alcanza a sí misma. Usa un atributo: [data-variante="destacada"].
La condición es sobre el marcado y está en el mismo componente. Si el ancestro que decide y el descendiente que reacciona están los dos dentro de tu componente y a dos niveles de distancia, un selector descendiente es más directo, más rápido de leer y funciona en todos los navegadores.
La variante cambia el contenido o la semántica. Si además de estilos hay que cambiar un aria-label, un texto o un orden de foco, la decisión no es de CSS y no debe vivir solo en una custom property.
El criterio, en una tabla
| Situación | Herramienta |
|---|---|
| Un ancestro desconocido comunica un modo a descendientes desconocidos | consulta de estilo |
| El propio elemento tiene una variante | atributo o clase |
| Ancestro y descendiente están en el mismo componente | selector descendiente |
| La condición depende del contenido del elemento | :has() |
| La condición depende del espacio disponible | consulta de tamaño |
| La condición depende del dispositivo o del usuario | media query |
Cuando adoptas este patrón estás creando, sin llamarlo así, una API de contexto para tu CSS: un conjunto de custom properties con nombres y valores acordados que cualquier ancestro puede fijar y cualquier descendiente puede consumir. Eso tiene las mismas virtudes y los mismos peligros que un contexto en cualquier sistema de componentes. La virtud es el desacoplamiento total entre productor y consumidor. El peligro es que es una dependencia invisible: no aparece en ninguna firma, no la detecta ningún analizador, y un componente que deja de funcionar porque nadie fijó --superficie no da ningún error, simplemente se ve mal en un sitio concreto. La medicina es exactamente la misma que aplicarías en código: enumera los valores admitidos, define un valor por defecto explícito en la raíz para que nunca falte, y escribe en algún sitio qué componentes leen qué propiedades. Un sistema de contexto sin documentación no es flexible, es adivinable, y la diferencia se paga entera el día que entra alguien nuevo al equipo o que vuelves tú seis meses después.