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

:not() con listas, y su aritmética

Qué significa exactamente negar una lista, por qué la versión encadenada casa con lo mismo pero pesa el doble, y cómo negar sin pagar especificidad.

⏱ 15 min

:not() es el más antiguo de los tres y el que más ha cambiado: en su versión original solo admitía un selector simple, y hoy acepta listas y selectores complejos completos. Ese ensanchamiento trajo un detalle que casi nadie tiene presente y que decide conflictos en producción: negar una lista y encadenar negaciones casan exactamente con los mismos elementos y tienen especificidades distintas. Elegir entre las dos formas es, por tanto, una decisión de arquitectura disfrazada de estilo de escritura.

🎯 Al terminar esta lección sabrás
  • Interpretar correctamente la semántica de :not() con una lista de argumentos.
  • Calcular la especificidad de las dos formas equivalentes y elegir la adecuada.
  • Usar selectores complejos dentro de :not().
  • Negar sin coste de especificidad y reconocer cuándo interesa.

Negar uno o varios

:not(X) casa con los elementos que no casan con X. Con una lista, casa con los que no casan con ninguno de sus argumentos:

/* Elementos de lista que no son el ultimo */
li:not(:last-child) { border-block-end: 1px solid; }

/* Enlaces que no son externos ni descargas */
a:not(.externo, [download]) { text-decoration-thickness: 2px; }

/* Botones activos: ni deshabilitados ni cargando */
.boton:not(:disabled, [aria-busy="true"]):hover {
  background: oklch(58% 0.19 258);
}

La confusión más frecuente es leer la coma como un “o” y esperar que :not(.aviso, .error) case con lo que no sea aviso o no sea error, que sería casi todo. La lectura correcta es la contraria: la negación se aplica a la lista entera, así que casa con lo que no es ninguna de las dos cosas. Formulado en lógica, :not(a, b) es la negación de la disyunción, y por tanto equivale a la conjunción de las negaciones.

La aritmética: lista frente a encadenado

De esa equivalencia lógica sale que estas dos reglas seleccionan el mismo conjunto de elementos:

p:not(.aviso, .error) { color: inherit; }
p:not(.aviso):not(.error) { color: inherit; }

Y sin embargo no pesan lo mismo. :not() toma la especificidad de su argumento más específico, así que:

Selector Cuenta Total
p:not(.aviso, .error) tipo más el máximo de la lista 0-1-1
p:not(.aviso):not(.error) tipo más una clase más otra clase 0-2-1

La segunda forma pesa el doble en la columna de clases. Con tres exclusiones serían tres clases, con cinco serían cinco, y cada una es un punto más que alguien tendrá que igualar para sobrescribirte.

Encadenar :not() es la escalada de especificidad disfrazada de precisión

Este es el caso donde el sistema de especificidad te tiende una trampa especialmente sutil, porque la forma que penaliza es la que parece más explícita y más legible. Escribir .boton:not(.primario):not(.fantasma):not(.enlace) se lee bien, expresa exactamente la intención, y sube el selector a 0-4-0 sin que su autor se dé cuenta de que ha hecho nada relacionado con la cascada. La versión con lista casa igual y se queda en 0-2-0. Y aquí viene lo incómodo: hay gente que encadena negaciones a propósito precisamente por eso, porque necesitaba ganarle a otra regla y descubrió que repetir :not() sube el peso sin cambiar lo que selecciona. Es exactamente el mismo movimiento que repetir una clase o anteponer body, con mejor prensa porque no parece un truco. La consecuencia es idéntica: sube el suelo del proyecto y obliga al siguiente a subir más. El criterio para no caer, ni por accidente ni por astucia, es este: usa la lista siempre que las exclusiones sean alternativas del mismo tipo, que es el noventa por ciento de los casos, y reserva el encadenado para cuando de verdad estés negando condiciones de naturaleza distinta y quieras que se lea así. Y si en algún momento te sorprendes eligiendo la forma encadenada porque la otra “no gana”, detente: ese es el síntoma de un conflicto de autoridad que se resuelve con capas, no con negaciones. Hay además una tercera forma que casi nadie conoce y que es la respuesta correcta cuando quieres excluir sin cobrar nada: envolver el argumento en :where(), que deja la negación en cero y te permite excluir todo lo que quieras sin que el selector engorde.

/* Excluye tres casos y sigue valiendo 0-1-0 */
.boton:not(:where(.primario, .fantasma, .enlace)) { border: 1px solid; }

Selectores complejos dentro de :not()

La versión moderna admite selectores complejos, con combinadores incluidos, y eso abre patrones que antes exigían clases auxiliares:

/* Parrafos que no estan dentro de un aviso */
p:not(.aviso p) { max-inline-size: 65ch; }

/* Imagenes que no son hijas directas de una figura */
img:not(figure > img) { border-radius: 0.5rem; }

/* Enlaces sin destino: probablemente marcadores de posicion */
a:not([href]) { color: inherit; cursor: default; }

Ten en cuenta que negar un selector complejo obliga al motor a comprobar ascendencia para cada candidato, lo que es más trabajo que negar una clase. En una página normal es irrelevante; en un documento con decenas de miles de nodos y muchas reglas de este tipo puede notarse, y conviene saberlo antes de construir un sistema entero sobre negaciones estructurales.

Dos trampas más

La lista de :not() no es tolerante. A diferencia de :is() y :where(), su lista es de las clásicas: si contiene un selector que el motor no entiende, se invalida la regla entera. Esto importa cuando mezclas pseudo-clases recientes:

/* Si el motor no conoce alguna, se pierde toda la regla */
input:not(:user-invalid, .invalido) { border-color: gray; }

/* Version defensiva: la lista tolerante va dentro */
input:not(:is(:user-invalid, .invalido)) { border-color: gray; }

Envolver el argumento en :is() recupera la tolerancia y, de paso, no cambia la especificidad, porque las dos toman el máximo de su lista.

Negar es a menudo la forma difícil de decir algo fácil. Muchos :not() son la señal de que el estado que estás excluyendo debería ser explícito. Cuando te descubras negando tres estados, considera declararlos con un atributo y seleccionar el que sí quieres:

/* En lugar de negar tres variantes */
.boton[data-variante="solido"] { border: 1px solid; }

El resultado es más corto, más rápido de leer y no depende de que la lista de exclusiones se mantenga al día cuando alguien añada una cuarta variante, que es el fallo silencioso clásico de los selectores construidos por negación.