wandres.dev
CONTAINER STYLE QUERIES · Consultar custom properties

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.

⏱ 17 min

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.

🎯 Al terminar esta lección sabrás
  • 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.

💡
El estado que ve el usuario también debe estar en el DOM

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
Lo que estás construyendo es un contexto, y los contextos hay que documentarlos

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.