Pseudo-clases avanzadas

Domina :is(), :where(), :not() y los selectores nth avanzados para escribir CSS más limpio, potente y con menos repetición.

En el curso de Fundamentals aprendiste :hover, :focus, :first-child. Las pseudo-clases modernas van mucho más lejos: te permiten agrupar selectores, construir reglas potentes y escribir CSS más limpio y mantenible.


Pseudo-clases modernas: la caja de herramientas definitiva

Las pseudo-clases modernas de CSS permiten escribir reglas potentes, selectivas y limpias. Aquí tienes las más importantes, con ejemplos y notas de uso real:

:is() — agrupar selectores sin repetir

:is(header, main) :is(h1, h2, h3) {
  color: var(--color-heading);
}

Especificidad: adopta la del selector más específico de la lista.

:where() — selector sin especificidad

:where(h1, h2, h3) { margin-top: 0; }

Especificidad: siempre 0. Útil para resets y utilidades.

:not() — excluir selectores

button:not(.primario) { opacity: 0.7; }
li:not(:last-child) { border-bottom: 1px solid var(--color-border); }
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]) { border: 2px solid var(--color-border); }
a:not(:is(.btn, .nav-link, [aria-current])) { text-decoration: underline; }

Notas: acepta listas y puede anidar :is().

:has() — seleccionar el padre según sus hijos

.card:has(img) { display: grid; grid-template-columns: 200px 1fr; }
label:has(input:focus) { color: var(--color-primary); font-weight: 700; }
form:has(:invalid) .submit-btn { opacity: 0.5; pointer-events: none; }
article:not(:has(h2)) { padding-top: 2rem; }

Notas: permite selectores de padres, muy útil en UI avanzada (menús, accordions, tabs, tooltips…). Más ejemplos prácticos en la lección de CSS Ninja: “Menús, tooltips y UI con :has, :focus-within, etc.”

:nth-child() y familia — seleccionar por posición

li:nth-child(odd) { background: #f5f5f5; }
li:nth-child(3n) { font-weight: bold; }
li:nth-child(3 of .highlight) { background: yellow; }
p:nth-child(-n+3 of p) { font-size: 1.1em; }

Notas: la versión moderna acepta un selector extra con “of”.

:focus-visible — foco solo con teclado

button:focus:not(:focus-visible) { outline: none; }
button:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }

Notas: mejora la accesibilidad y la experiencia de usuario.

:empty, :blank, :placeholder-shown

.notification-badge:empty { display: none; }
input:placeholder-shown + label { transform: translateY(0); font-size: 1rem; color: var(--color-text-muted); }
input:not(:placeholder-shown) + label { transform: translateY(-1.5rem); font-size: 0.75rem; color: var(--color-primary); }

Nota: Las pseudo-clases modernas se usan en componentes avanzados como menús, accordions y tabs. Consulta ejemplos prácticos en las lecciones de CSS Ninja: “Menús, tooltips y UI con :has, :focus-within, etc.” y “Accordions y tabs sin JS”.


Nota: Si buscas selectores de padres por sus hijos, como :has, lo tienes en el curso CSS Ninja, bloque “De JS a CSS”.

:is(.card, p) { color: red; }
/* especificidad: 0,1,0 — porque .card tiene especificidad de clase */

:where() — cero especificidad

:where() funciona igual que :is() pero con especificidad cero. Útil para estilos base que quieres que cualquier otro CSS pueda sobreescribir fácilmente:

/* Estilos reset — sin especificidad para no competir con nada */
:where(h1, h2, h3, h4, h5, h6) {
  font-weight: bold;
  line-height: 1.2;
}

/* Este selector puede sobreescribirlo sin necesidad de más especificidad */
.article h2 {
  line-height: 1.5;
}

La diferencia clave:

Especificidad
:is(.card, p)La del selector más específico de la lista
:where(.card, p)Siempre 0

:not() — excluir elementos

:not() excluye los elementos que coincidan con el selector interior. En la versión moderna acepta listas:

/* Sin el último elemento */
li:not(:last-child) {
  border-bottom: 1px solid var(--color-border);
}

/* Todos los inputs excepto checkbox, radio y submit */
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]) {
  border: 2px solid var(--color-border);
  padding: 0.5rem;
}

/* Lista compleja con :is() dentro de :not() */
a:not(:is(.btn, .nav-link, [aria-current])) {
  text-decoration: underline;
}

:has() — seleccionar el padre

:has() es la pseudo-clase más esperada de los últimos años. Permite seleccionar un elemento en función de lo que contiene. Siempre se quiso “selector de padre” y aquí está:

/* .card que contiene una imagen */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

/* label que contiene un input en foco */
label:has(input:focus) {
  color: var(--color-primary);
  font-weight: 700;
}

/* form que tiene campos inválidos */
form:has(:invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* artículo sin un h2 — estilo de reserva */
article:not(:has(h2)) {
  padding-top: 2rem;
}

:has() es compatible con todos los navegadores modernos desde 2023. No lo uses en proyectos que requieran soporte de navegadores muy antiguos.


:nth-child() con selector

La versión moderna de :nth-child() acepta un selector como argumento adicional, algo que antes era imposible:

/* El tercer li que tenga clase .highlight */
li:nth-child(3 of .highlight) {
  background: yellow;
}

/* Los primeros 3 elementos párrafo (ignorando otros tipos) */
p:nth-child(-n+3 of p) {
  font-size: 1.1em;
}

Sin of, :nth-child(3) cuenta todos los hijos y selecciona el tercero si también coincide con el selector. Con of, solo cuenta los que coinciden con el tipo especificado.


:focus-visible — foco solo con teclado

:focus se activa con ratón y teclado. :focus-visible solo se activa cuando el foco es visible y relevante (navegación por teclado), no al hacer clic:

/* Quitar el outline molesto al hacer clic con ratón */
button:focus:not(:focus-visible) {
  outline: none;
}

/* Pero mantenerlo para la navegación por teclado */
button:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
}

:empty, :blank y :placeholder-shown

/* Ocultar un contenedor si no tiene contenido */
.notification-badge:empty {
  display: none;
}

/* Etiquetar inputs mientras no tienen texto (efecto placeholder flotante) */
input:placeholder-shown + label {
  transform: translateY(0);
  font-size: 1rem;
  color: var(--color-text-muted);
}

input:not(:placeholder-shown) + label {
  transform: translateY(-1.5rem);
  font-size: 0.75rem;
  color: var(--color-primary);
}

En la siguiente lección trabajamos los pseudo-elementos: ::before, ::after, ::placeholder, ::selection y cómo usarlos para añadir decoración y contenido sin tocar el HTML.