Tema
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órmula | Selecciona |
|---|---|
3 | Sólo el 3.º |
odd o 2n+1 | Impares: 1, 3, 5... |
even o 2n | Pares: 2, 4, 6... |
3n | Cada tercero: 3, 6, 9... |
3n+1 | 1, 4, 7, 10... |
n+4 | Del 4.º en adelante |
-n+3 | Los 3 primeros |
n+3 y -n+6 encadenados | Del 3.º al 6.º (ver ejemplo abajo) |
Prueba cualquier fórmula:
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
- Pinta las filas impares de una tabla con un gris muy claro y resalta la fila sobre la que pasa el mouse.
- Añade un asterisco rojo después de los
labelcon clase.obligatorio. - Selecciona los 3 primeros elementos de una lista y ponlos en negrita.
- Muestra un borde rojo en los inputs inválidos, pero sólo después de que el usuario haya escrito.
- 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-visiblepara anillos de foco accesibles y:focus-withinpara resaltar contenedores. :nth-child(an+b)selecciona por posición;odd,even,-n+3,n+4son las fórmulas más útiles.:not(),:is(),:where()y:has()simplifican selectores.:user-invalidvalida formularios sin molestar antes de tiempo.::before/::afternecesitancontenty son para decoración.
Siguiente: Transiciones y transformaciones.