wandres.dev
SELECTORES MODERNOS · :is, :where y la especificidad cero

Diseñar CSS para que otros lo sobrescriban

Las tres superficies de sobrescritura ordenadas por preferencia, la distinción entre opiniones y mecánica, y un componente completo escrito con los criterios de todo el bloque.

⏱ 18 min

Todo lo aprendido en estos siete niveles converge en una sola decisión de diseño: qué le ofreces a quien vaya a modificar tu CSS. Si la respuesta es “que iguale mis selectores”, has construido algo que se romperá en la primera refactorización. Si es “que redefina estas variables y, si necesita más, que ponga su capa por encima”, has construido algo que sobrevive. Esta lección cierra el bloque con los criterios concretos y con un componente escrito según ellos.

🎯 Al terminar esta lección sabrás
  • Ordenar las tres superficies de sobrescritura por coste y acoplamiento.
  • Separar las declaraciones que son opinión de las que son mecánica.
  • Escribir un componente con su superficie pública explícita.
  • Verificar antes de publicar que el CSS es sobrescribible sin conocer su interior.

Tres superficies, en orden de preferencia

La primera y la mejor: custom properties. Quien te use define un valor y tú lo lees. No hay competición en la cascada porque no hay dos declaraciones peleando por la misma propiedad del mismo elemento: la variable se resuelve donde se declaró y viaja por herencia hasta tu punto de uso. El acoplamiento se reduce a los nombres de las variables, que es lo único que tienes que mantener estable, y por dentro puedes reescribir el componente entero.

La segunda: clases y atributos que tú publicas. Variantes, tonos, tamaños, estados. Son un contrato explícito y razonablemente estable, con el coste de que su combinatoria crece y de que cada una es una decisión que ya no puedes retirar sin romper.

La tercera y la que hay que evitar: tus selectores. Si la única forma de cambiar algo es escribir un selector que gane al tuyo, has convertido la estructura interna de tu implementación en superficie pública. Cualquier reorganización tuya rompe a tus consumidores, y ellos ni siquiera sabrán por qué, porque nada falla: simplemente empieza a ganar otra regla.

El orden importa porque conviene diseñar de arriba abajo: primero decide qué será configurable con variables, después qué necesita variante propia, y solo entonces escribe los selectores, sabiendo que nadie debería tener que mirarlos.

Opiniones y mecánica

Aquí está la distinción que hace que el sistema no se rompa, y no aparece en ninguna guía de estilo.

Una declaración es mecánica si quitarla rompe el componente: el display que establece el contexto de layout, el position que hace posible un anclaje, el overflow que crea el área de desplazamiento. Una declaración es opinión si quitarla solo lo deja más feo: colores, espaciados, radios, sombras, pesos tipográficos.

Un componente entero a especificidad cero se rompe con un selector de tipo ajeno

Este es el error de diseño que se comete al aprender :where() y entusiasmarse, y la aritmética de por qué falla es contundente. Un selector envuelto en :where() vale 0-0-0. Un selector de tipo cualquiera vale 0-0-1. Por tanto un simple div en el CSS de tu consumidor le gana a toda tu biblioteca. No hace falta que nadie te apunte: basta un reset genérico de los que abundan, un button { all: unset }, un ul { padding: 0 }, un div { display: block } heredado de un proyecto anterior. Si has envuelto en :where() la declaración que establecía tu contexto de layout, ese div ajeno la desactiva y tu componente no se ve raro: se desmonta, y el informe de error que recibirás dirá que tu biblioteca no funciona en su proyecto, sin ninguna pista más. Lo peor es que la causa es invisible desde los dos lados: tu CSS es correcto, el suyo es correcto, y el conflicto está en una capa de abstracción que ninguno de los dos está mirando. La regla que evita esto entera cabe en una frase: las opiniones van a cero, la mecánica no. Todo lo que sea gusto —color, relleno, radio, sombra— envuélvelo en :where() para que cualquiera lo cambie con una clase. Todo lo que sea estructura déjalo con su especificidad normal de una clase, 0-1-0, que ya es suficientemente baja para que se pueda sobrescribir a propósito y suficientemente alta para que no se desactive por accidente. Y si además publicas dentro de una capa con nombre, tu consumidor tiene la vía limpia para anular incluso la mecánica cuando de verdad lo necesite, sin que ningún reset genérico pueda hacerlo sin querer.

Un componente completo

Con todos los criterios del bloque aplicados a la vez: capa raíz con subcapas, mecánica a especificidad normal, opiniones a cero, variables como superficie pública y variantes por atributo.

@layer notas.base, notas.variantes;

@layer notas.base {
  /* Mecanica: quitar esto rompe el componente, no va a cero */
  .nota {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: start;
  }

  /* Opiniones: a cero, cualquiera las cambia con una clase */
  :where(.nota) {
    gap: var(--nota-gap, 0.75rem);
    padding: var(--nota-relleno, 1rem);
    border-inline-start: 3px solid var(--nota-borde, oklch(70% 0.08 250));
    border-radius: var(--nota-radio, 0.5rem);
    background: var(--nota-fondo, oklch(96% 0.02 250));
  }

  :where(.nota > .titulo) {
    font-weight: 600;
    text-wrap: balance;
  }

  /* No declaramos color: que lo herede del contexto */
}

@layer notas.variantes {
  .nota[data-tono="aviso"] {
    --nota-fondo: oklch(96% 0.05 85);
    --nota-borde: oklch(70% 0.14 85);
  }

  .nota[data-tono="error"] {
    --nota-fondo: oklch(95% 0.04 25);
    --nota-borde: oklch(62% 0.19 25);
  }
}

Repasa las decisiones, porque cada una viene de un nivel distinto de este bloque.

display y grid-template-columns van sin envolver: son mecánica, y un reset ajeno no debe poder desmontarlas.

El resto va en :where(), de modo que un consumidor cambia el relleno con .mi-nota { padding: 2rem; } y gana con 0-1-0 sin conocer nada de la implementación.

Cada opinión se lee de una variable con valor de reserva en el punto de uso, para que la ausencia de la variable no produzca un valor inválido en tiempo de computación, que sería un fallo mucho peor que un valor por defecto.

Las variantes cambian variables, no propiedades. Así una variante no compite con nada y se pueden combinar sin pensar en el orden.

El color no se declara en ningún sitio, de modo que el componente funciona dentro de una zona clara y de una zona oscura sin variantes adicionales.

Y todo vive en una capa con nombre, así que quien lo consuma puede reubicar el conjunto entero en su jerarquía con una línea:

@layer reset, notas, componentes, utilidades;

Antes de publicar

Cinco comprobaciones que detectan casi todos los problemas de sobrescritura antes de que los descubra otro.

Sobrescribe tu propio componente con una sola clase desde un fichero cargado después. Si necesitas dos clases o !important, tienes especificidad de sobra en alguna regla.

Aplica un reset genérico agresivo por encima —selectores de tipo sobre los elementos que usas— y comprueba que el componente sigue montado. Lo que se rompa es mecánica que estaba a cero.

Quita todas las variables y comprueba que el resultado sigue siendo usable. Si algo desaparece o cambia de color a un valor heredado extraño, falta un valor de reserva.

Métete el componente en un contexto oscuro sin añadir ninguna variante. Si el texto se vuelve ilegible, hay un color declarado que debería heredarse.

Documenta la lista de variables y el orden de capas en el mismo sitio donde documentas las clases. Las dos cosas son superficie pública y las dos rompen a tus consumidores si cambian.

⚔️ Reescribe un componente tuyo con estos criterios
  1. Coge un componente propio y clasifica cada declaración en mecánica u opinión.
  2. Envuelve las opiniones en :where() y déjalas leyendo variables con reserva.
  3. Convierte las variantes para que solo cambien variables, y comprueba que dos variantes se pueden combinar sin que el orden importe.
  4. Pásale las cinco comprobaciones de la lista y arregla lo que falle.