:where() y la especificidad cero
El selector que acota sin puntuar, por qué es la llave del CSS pensado para que otros lo usen, y qué criterio pasa a decidir cuando todo vale cero.
:where() hace exactamente lo mismo que :is() con una diferencia: su especificidad es siempre cero, pase lo que pase dentro. Suena a detalle menor y es la separación conceptual más importante que ha recibido el lenguaje de selectores, porque por primera vez permite decir dónde se aplica una regla sin decir a la vez cuánto manda. Esas dos cosas llevaban veinticinco años atadas en un mismo número, y desatarlas cambia por completo cómo se escribe CSS destinado a ser usado por otros.
- Usar
:where()para acotar el alcance de una regla sin aumentar su especificidad. - Escribir estilos por defecto que cualquiera pueda sobrescribir con una sola clase.
- Combinar
:where()con capas y saber qué resuelve cada uno. - Reconocer qué criterio de la cascada decide cuando la especificidad queda a cero.
Cero, siempre
La regla no tiene matices: la especificidad de :where() es 0-0-0, con independencia de lo que contenga. Puedes meter identificadores, cadenas de siete niveles o cualquier combinación, y el resultado sigue siendo cero.
/* 0-0-0 */
:where(#app .panel .lista .item) a { color: teal; }
/* 0-1-0: una sola clase le gana */
.enlace-destacado { color: crimson; }
Y el cero se propaga hacia fuera cuando :where() es el argumento de otra pseudo-clase funcional, porque esas toman la especificidad de su argumento más específico:
/* 0-0-1: el :not() entero vale cero */
a:not(:where(.externo, .descarga)) { text-decoration-thickness: 2px; }
Con esa combinación puedes escribir exclusiones arbitrariamente complicadas sin pagar un solo punto, que es algo que antes no se podía hacer.
La llave del CSS de librería
El escenario que justifica la existencia del selector es este: escribes CSS que otros van a usar, y quieres dos cosas a la vez que hasta ahora eran incompatibles.
Quieres acotar con precisión, porque un selector vago se aplica donde no debe. Y quieres que sobrescribirte sea trivial, porque tu consumidor no debería tener que estudiar tu implementación para cambiar un color.
Sin :where() había que elegir. Un selector preciso como .menu > .item > .enlace:hover vale 0-3-0 y obliga a quien te use a igualar tres clases, para lo cual tiene que leer tu código y acoplarse a su estructura. Un selector vago como a:hover es sobrescribible pero se aplica a enlaces que no son tuyos.
Con :where() no hay que elegir:
:where(.menu > .item > .enlace):hover {
color: oklch(52% 0.18 258);
}
Acota exactamente igual y vale 0-0-0. Cualquiera puede sobrescribirlo con una clase, y si mañana cambias tu estructura interna, nadie se rompe porque nadie ha tenido que copiar tu selector.
De ahí sale una regla de publicación que conviene adoptar entera: todo lo que sea un valor por defecto va envuelto en :where(). Un reset, la tipografía base, los estilos iniciales de un componente. La prueba es sencilla de formular: si la respuesta a “¿qué pasa si alguien sobrescribe esto?” es “nada malo, es su decisión”, entonces debe estar a cero.
:where(.prosa) :where(h2, h3, h4) {
margin-block-start: 1.5em;
text-wrap: balance;
}
:where(.prosa) :where(ul, ol) {
padding-inline-start: 1.5em;
}
Aquí está el efecto secundario que casi nadie anticipa y que conviene entender antes de envolverlo todo. :where() no elimina los conflictos: los empuja hacia abajo en la lista de criterios de la cascada. Si dos reglas apuntan al mismo elemento y las dos valen cero, la especificidad empata, y el desempate cae en el último criterio disponible, que es el orden de aparición. Y el orden de aparición es, de los seis criterios, el único que no controlas tú del todo: lo controla el orden de tus etiquetas de enlace, el orden en que tu empaquetador concatena los módulos, y en una aplicación con división de código, el orden en que el usuario navegó. Es decir, poner todo a cero puede convertir un sistema en el que ganaba una regla concreta y comprensible en otro en el que gana la que llegó última, que es una respuesta no determinista. El síntoma es reconocible: estilos que dependen de por dónde entró el usuario al sitio y que no se reproducen recargando. Por eso :where() no es una arquitectura por sí solo; es media arquitectura. La otra mitad son las capas: @layer fija la autoridad entre bloques de forma explícita e independiente del orden de carga, y :where() se ocupa de que dentro de cada bloque nadie tenga que pelear. Usados juntos, la especificidad deja de ser un factor y el sistema es a la vez sobrescribible y determinista. Usado :where() solo, en un proyecto grande y con carga fragmentada, has cambiado un problema de escalada por un problema de carrera, que es más difícil de diagnosticar porque no deja rastro en el código. La regla es corta: si envuelves en :where(), declara capas.
Repartirse el trabajo con las capas
Las dos herramientas se solapan en apariencia y resuelven cosas distintas, y tenerlo claro evita usarlas mal.
@layer decide quién manda entre bloques. Es una decisión de arquitectura, se declara una vez y afecta a todo el proyecto.
:where() decide cuánto pesa un selector concreto. Es una decisión local, se escribe en cada regla y afecta solo a ella.
De ahí salen dos consejos que parecen contradictorios y no lo son.
Dentro de tu propio proyecto con capas, :where() es casi innecesario. Si tu reset está en la capa reset, ya pierde contra todo por estar donde está, y envolverlo además en :where() no aporta nada salvo ruido.
En el CSS que publiques para otros, :where() es imprescindible. No sabes si tu consumidor usa capas, y no puedes obligarle. :where() funciona en todos los escenarios, incluido el de quien carga tu hoja con una etiqueta de enlace y escribe su CSS a continuación.
La combinación completa para una biblioteca es envolver el paquete en una capa raíz y escribir los valores por defecto con :where(): quien use capas te reubica donde quiera, y quien no las use te sobrescribe igualmente con una clase.
Dónde no ponerlo
Hay tres sitios donde envolver en :where() es un error.
En lo que no debe sobrescribirse. Si una regla es estructural —si quitarla rompe el componente en lugar de cambiar su aspecto— dejarla a cero significa que cualquier declaración perdida de un tercero la desactiva sin querer. Eso no es flexibilidad, es fragilidad.
En utilidades. Una utilidad existe para forzar un valor; a cero no fuerza nada. Las utilidades van al final del orden de capas, o importantes al principio, pero nunca envueltas.
Alrededor del sujeto del selector cuando el sujeto es el que da sentido a la regla. Envolver la parte que acota está bien; envolver la parte que identifica lo que estás estilando suele indicar que el selector se podía escribir más simple.
Con :is() y :where() cubiertos, queda la tercera de la familia, que niega en lugar de agrupar y que tiene su propia aritmética: :not().