Skip to content

Pseudo-clases y pseudo-elementos ​

¿Cómo cambias el color de un botón al pasar el mouse, pintas filas alternas de una tabla, marcas un campo inválido o añades un ícono decorativo sin tocar el HTML? Con pseudo-clases y pseudo-elementos. Son la herramienta que da vida e interacción a tus interfaces con CSS puro.

Concepto ​

  • Una pseudo-clase (:) selecciona un elemento según su estado o posición: "el enlace cuando tiene el mouse encima", "el primer elemento de la lista", "el campo cuando es inválido".
  • Un pseudo-elemento (::) selecciona o crea una parte de un elemento: "la primera letra", "un adorno antes del contenido", "el texto seleccionado".

Analogía

Una pseudo-clase es como decir "la persona que está sentada" (un estado). Un pseudo-elemento es como decir "el sombrero de esa persona" (una parte).

Pseudo-clases de interacción ​

css
a:hover        { color: #7c3aed; }          /* mouse encima */
a:active       { color: #5b21b6; }          /* mientras se presiona */
a:visited      { color: #6b21a8; }          /* enlace ya visitado */
input:focus    { border-color: #7c3aed; }   /* tiene el foco (clic o teclado) */

:focus vs. :focus-visible ​

:focus se activa con mouse y teclado; :focus-visible sólo cuando el navegador considera que el usuario necesita ver el foco (normalmente al navegar con teclado). Es la opción moderna para anillos de foco:

css
.boton:focus-visible {
  outline: 3px solid #7c3aed;
  outline-offset: 3px;
}

Así los usuarios de mouse no ven el anillo al hacer clic, pero quienes usan teclado sí.

:focus-within ​

Se activa en un padre cuando algún hijo tiene el foco:

css
.buscador:focus-within {
  box-shadow: 0 0 0 3px rgb(124 58 237 / 0.3);   /* resalta todo el buscador */
}

Orden LVHA

Si estilizas los estados de un enlace, escríbelos en orden Link, Visited, Hover, Active (:link, :visited, :hover, :active). Como tienen la misma especificidad, el orden importa.

Pseudo-clases estructurales ​

css
li:first-child      { font-weight: bold; }   /* el primer hijo */
li:last-child       { border-bottom: 0; }    /* el último */
li:only-child       { }                      /* si es el único hijo */
p:first-of-type     { font-size: 1.2rem; }   /* el primer <p> entre sus hermanos */
img:last-of-type    { }
.vacia:empty        { display: none; }       /* sin contenido (ni siquiera espacios) */

:nth-child() y sus fórmulas ​

:nth-child(an+b) selecciona según la posición, donde n toma los valores 0, 1, 2, 3...

FórmulaSelecciona
3Sólo el 3.º
odd o 2n+1Impares: 1, 3, 5...
even o 2nPares: 2, 4, 6...
3nCada tercero: 3, 6, 9...
3n+11, 4, 7, 10...
n+4Del 4.º en adelante
-n+3Los 3 primeros
n+3 y -n+6 encadenadosDel 3.º al 6.º (ver ejemplo abajo)

Prueba cualquier fórmula:

Probador de :nth-child
css
/* Filas alternas en una tabla (zebra) */
tbody tr:nth-child(even) {
  background: #f8fafc;
}

/* Del 3.º al 6.º elemento */
li:nth-child(n+3):nth-child(-n+6) {
  color: crimson;
}

/* Contando desde el final: los 2 últimos */
li:nth-last-child(-n+2) {
  opacity: 0.6;
}

/* nth-child "of S": el 2.º elemento que tenga la clase .destacado */
li:nth-child(2 of .destacado) { }

nth-child vs. nth-of-type

p:nth-child(2) significa "el segundo hijo, si además es un p". Si el segundo hijo es un h2, no selecciona nada. p:nth-of-type(2) significa "el segundo p entre sus hermanos", sin importar qué otros elementos haya.

Pseudo-clases lógicas ​

css
/* :not() — todo excepto */
.menu a:not(.activo) { opacity: 0.7; }
li:not(:last-child)  { border-bottom: 1px solid #e5e7eb; }   /* separadores entre ítems */

/* :is() — agrupa selectores largos */
:is(h1, h2, h3):hover { color: #7c3aed; }
.articulo :is(h2, h3) { margin-top: 2em; }

/* :where() — igual que :is() pero especificidad 0 */
:where(ul, ol)[class] { list-style: none; padding: 0; }

/* :has() — "el padre que contiene..." (lo verás a fondo en CSS moderno) */
.tarjeta:has(img) { padding-top: 0; }

Pseudo-clases de formularios ​

css
input:checked + label   { font-weight: bold; }     /* checkbox o radio marcado */
input:disabled          { opacity: 0.5; cursor: not-allowed; }
input:required          { border-left: 3px solid #f59e0b; }
input:optional          { }
input:read-only         { background: #f1f5f9; }

/* Validación nativa */
input:valid             { border-color: #16a34a; }
input:invalid           { border-color: #dc2626; }

/* Mejor: sólo después de que el usuario interactuó */
input:user-invalid      { border-color: #dc2626; }

/* El placeholder es visible = el campo está vacío */
input:placeholder-shown { }

Error común con :invalid

:invalid se aplica desde que carga la página: un formulario vacío aparece lleno de bordes rojos antes de que el usuario escriba nada. Usa :user-invalid (todos los navegadores modernos), que sólo se activa tras interactuar con el campo.

Label flotante con :placeholder-shown ​

html
<div class="campo">
  <input id="email" type="email" placeholder=" " required>
  <label for="email">Correo electrónico</label>
</div>
css
.campo {
  position: relative;
}

.campo input {
  width: 100%;
  padding: 1.4rem 0.75rem 0.5rem;
  border: 1px solid #cbd5e1;
  border-radius: 8px;
}

.campo label {
  position: absolute;
  left: 0.75rem;
  top: 1rem;
  color: #64748b;
  pointer-events: none;            /* el clic atraviesa el label y llega al input */
  transition: 0.2s;
}

/* Si tiene foco o tiene texto (el placeholder " " NO se ve), el label sube */
.campo input:focus + label,
.campo input:not(:placeholder-shown) + label {
  top: 0.35rem;
  font-size: 0.75rem;
  color: #7c3aed;
}

Otras pseudo-clases útiles ​

css
:target           { background: #fef9c3; }   /* el elemento cuyo id está en la URL (#seccion) */
:root             { }                        /* el elemento html */
:lang(en)         { quotes: "“" "”"; }
::backdrop        { }                        /* fondo de un <dialog> modal (es pseudo-elemento) */

Pseudo-elementos ​

::before y ::after ​

Crean un elemento "virtual" como primer o último hijo del elemento. Requieren la propiedad content (aunque sea vacía):

css
.requerido::after {
  content: " *";
  color: #dc2626;
}

.cita::before {
  content: "“";
  font-size: 4rem;
  line-height: 0;
  color: #cbd5e1;
}

/* Elemento decorativo vacío: una línea bajo el título */
.titulo-seccion::after {
  content: "";
  display: block;
  width: 60px;
  height: 4px;
  margin-top: 0.5rem;
  border-radius: 2px;
  background: #7c3aed;
}

Usos típicos: íconos decorativos, comillas, separadores, capas de overlay, tooltips, efectos de hover.

css
/* Overlay sobre una imagen al pasar el mouse */
.foto {
  position: relative;
}
.foto::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(124 58 237 / 0.4);
  opacity: 0;
  transition: opacity 0.3s;
}
.foto:hover::after {
  opacity: 1;
}

/* Tooltip con atributo data- */
.tooltip {
  position: relative;
}
.tooltip::after {
  content: attr(data-tip);         /* toma el texto del atributo */
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  translate: -50% 0;
  padding: 4px 8px;
  border-radius: 6px;
  background: #0f172a;
  color: #fff;
  font-size: 0.75rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}
.tooltip:hover::after,
.tooltip:focus-visible::after {
  opacity: 1;
}
html
<button class="tooltip" data-tip="Copiar al portapapeles">📋</button>

Accesibilidad con content

El texto en content no es seleccionable y los lectores de pantalla lo leen de forma inconsistente. Úsalo sólo para decoración. La información importante va en el HTML.

Elementos sin ::before

Los elementos "reemplazados" como img, input o br no admiten ::before/::after, porque no tienen contenido interno. Envuélvelos en un contenedor si lo necesitas.

Otros pseudo-elementos ​

css
input::placeholder  { color: #94a3b8; font-style: italic; }
::selection         { background: #7c3aed; color: #fff; }      /* texto seleccionado */
li::marker          { color: #7c3aed; content: "✓ "; }         /* viñeta de la lista */
p::first-letter     { float: left; font-size: 3.5em; line-height: 0.9; margin-right: 0.1em; }  /* letra capital */
p::first-line       { font-weight: 600; }
dialog::backdrop    { background: rgb(0 0 0 / 0.6); backdrop-filter: blur(3px); }
input[type="file"]::file-selector-button { border: 0; padding: 0.5rem 1rem; background: #7c3aed; color: #fff; border-radius: 6px; }

Casos de uso reales ​

css
/* Menú: separadores entre enlaces, excepto el último */
.menu li:not(:last-child)::after {
  content: "·";
  margin-inline: 0.75rem;
  color: #cbd5e1;
}

/* Enlace activo de la navegación */
.menu a[aria-current="page"] {
  color: #7c3aed;
  font-weight: 700;
}

/* Subrayado animado en hover */
.enlace {
  position: relative;
  text-decoration: none;
}
.enlace::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 100%;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.3s;
}
.enlace:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

Ejercicios ​

  1. Pinta las filas impares de una tabla con un gris muy claro y resalta la fila sobre la que pasa el mouse.
  2. Añade un asterisco rojo después de los label con clase .obligatorio.
  3. Selecciona los 3 primeros elementos de una lista y ponlos en negrita.
  4. Muestra un borde rojo en los inputs inválidos, pero sólo después de que el usuario haya escrito.
  5. Crea un título con una línea decorativa de 50px debajo usando ::after.
Ver solución
css
/* 1 */
tbody tr:nth-child(odd) { background: #f8fafc; }
tbody tr:hover { background: #ede9fe; }

/* 2 */
label.obligatorio::after {
  content: " *";
  color: #dc2626;
}

/* 3 */
li:nth-child(-n+3) { font-weight: bold; }

/* 4 */
input:user-invalid { border: 2px solid #dc2626; }

/* 5 */
.titulo::after {
  content: "";
  display: block;
  width: 50px;
  height: 3px;
  margin-top: 8px;
  background: currentColor;
}

Resumen ​

  • Pseudo-clases (:) = estados o posiciones; pseudo-elementos (::) = partes o elementos virtuales.
  • Usa :focus-visible para anillos de foco accesibles y :focus-within para resaltar contenedores.
  • :nth-child(an+b) selecciona por posición; odd, even, -n+3, n+4 son las fórmulas más útiles.
  • :not(), :is(), :where() y :has() simplifican selectores.
  • :user-invalid valida formularios sin molestar antes de tiempo.
  • ::before/::after necesitan content y son para decoración.

Siguiente: Transiciones y transformaciones.

Academia Web — aprende, copia y construye.