Los selectores lógicos y relacionales como :not(), :is(), :where() y :has() son pseudo-clases para expresar condiciones de selección en CSS moderno.
Si habéis llegado hasta aquí, ya sabéis seleccionar por etiqueta, clase, ID, estado y posición. Con eso se puede hacer muchísimo. Aun así, CSS tiende a llenarse de selectores repetidos, excepciones sueltas y clases que solo existen para salvar una regla concreta.
Estas pseudo-clases nos ayudan a escribir selectores que dicen mejor lo que queremos: esto sí, esto no, cualquiera de estos, o este elemento si contiene aquello.
Son una de las mejores y más útiles incorporaciones que se han hecho a CSS. Así que vamos a verlas en detalle 👇.
Excluir con :not()
La pseudo-clase
.menu a:not(.activo) {
color: #64748b;
}En este ejemplo damos estilo a todos los enlaces del menú excepto al que tiene la clase .activo.
También podemos usarla para evitar aplicar un margen al último elemento de una lista.
.lista li:not(:last-child) {
margin-bottom: 0.75rem;
}Eso se lee bastante bien: “todos los li que no sean el último hijo”. No hace falta meter una clase .ultimo en el HTML, que sería un poco triste.
:not() es muy cómodo para excepciones pequeñas. Si el selector empieza a ser demasiado largo, probablemente os conviene usar una clase más clara.
Agrupar opciones con :is()
La pseudo-clase
article :is(h2, h3, h4) {
margin-top: 2rem;
}Sin :is(), tendríamos que escribir:
article h2,
article h3,
article h4 {
margin-top: 2rem;
}La diferencia no parece enorme aquí, pero en selectores largos ahorra bastante ruido.
:is(header, main, footer) :is(h1, h2) {
color: darkblue;
}Esta regla apunta a h1 y h2 que estén dentro de header, main o footer. Es la misma idea que escribir todas las combinaciones a mano, solo que sin convertir la hoja de estilos en una sopa de comas.
Bajar especificidad con :where()
:is(), pero tiene una particularidad muy útil: no suma especificidad.
:where(article, section) :where(h2, h3) {
margin-top: 2rem;
color: #1f2937;
}Esto encaja muy bien en hojas de estilo globales, donde queremos dar una base razonable sin crear selectores pesados. Luego, si un componente concreto necesita cambiar ese color, una simple clase podrá sobrescribirlo sin pelearse con la especificidad.
.card h2 {
color: #dc2626;
}La diferencia práctica es esta: :is() agrupa y mantiene la especificidad del selector más fuerte que tenga dentro. :where() agrupa, pero su especificidad es siempre cero.
Seleccionar por contenido con :has()
La pseudo-clase
Durante mucho tiempo, CSS solo podía ir de padres a hijos. Podíamos seleccionar .card img, pero no seleccionar .card dependiendo de si tenía una imagen dentro.
Con :has(), eso ya se puede expresar directamente.
/* Selecciona la .card solo si tiene una imagen dentro */
.card:has(img) {
background-color: #f0f0f0;
border: 2px solid blue;
}Esto abre patrones muy interesantes para componentes, formularios y estados visuales.
/* El formulario cambia de aspecto si tiene un input inválido */
form:has(input:invalid) {
border-color: red;
box-shadow: 0 0 10px rgba(255, 0, 0, 0.2);
}También podemos usarlo para mejorar pequeños componentes sin añadir clases extra al HTML.
.campo:has(input:focus) {
border-color: #2563eb;
background-color: #eff6ff;
}Aquí el contenedor .campo reacciona cuando el input interior recibe foco. Antes esto solía requerir JavaScript o clases añadidas a mano.
Combinarlos con cabeza
Estas pseudo-clases se pueden combinar entre sí. Eso permite escribir condiciones bastante expresivas, siempre que no se nos vaya la mano.
section:has(.comprar) :is(h2, h3):not(.oculto) {
color: #92400e;
}La regla se lee así: dentro de una sección que tenga un elemento .comprar, selecciona títulos h2 o h3 que no tengan la clase .oculto.
También podemos usar :where() para dejar estilos base muy fáciles de sobrescribir.
:where(.post, .pagina) :is(h2, h3):not(:first-child) {
margin-top: 2rem;
}No intentéis demostrar todo lo que sabéis en un único selector. Si una regla necesita cinco minutos de meditación para entenderse, seguramente sería mejor usar una clase.