Pseudoclases y pseudoelementos en CSS
En esta lección
En la lección de selectores viste un repaso rápido de las pseudoclases y los pseudoelementos. Aquí vamos a fondo: los estados de un elemento (ratón encima, foco, pulsado), los estados de los formularios, la posición dentro del HTML con la fórmula de :nth-child() y las “partes” de un elemento que puedes estilizar o crear desde CSS. Con ellas podrás dar estilo a muchísimas situaciones sin añadir ni una clase al HTML.
Pseudoclase o pseudoelemento
- Una pseudoclase selecciona un elemento en un estado o en una posición concreta: el enlace con el ratón encima, el tercer
<li>, el campo vacío. Se escribe con dos puntos:a:hover. - Un pseudoelemento selecciona una parte de un elemento, o crea una que no existe en el HTML: la primera letra, el marcador de la lista, un adorno antes del texto. Se escribe con cuatro puntos (dos pares):
p::first-letter.
li:first-child { color: green; } /* pseudoclase: el li que es el primero */
li::marker { color: green; } /* pseudoelemento: el número o la viñeta del li */
Puedes combinarlos, pero el pseudoelemento va siempre al final: li:first-child::after es válido; li::after:first-child, no.
Estados de interacción
:hover y :active
:hover se activa cuando el puntero está encima; :active, mientras se mantiene pulsado:
.boton { background: #2d6cdf; }
.boton:hover { background: #1b4bb0; }
.boton:active { transform: translateY(1px); } /* parece que se hunde */
En móviles no hay “ratón encima”: el :hover se queda pegado tras tocar o no se ve. Nunca escondas información importante detrás de un :hover. Si quieres aplicarlo solo en dispositivos con ratón, usa @media (hover: hover) (lo tienes en responsive).
:focus, :focus-visible y :focus-within
Un elemento tiene el foco cuando es el que recibe el teclado: el campo donde escribes o el botón al que llegaste con la tecla Tab.
input:focus { border-color: #2d6cdf; }
button:focus-visible {
outline: 3px solid #2d6cdf;
outline-offset: 3px;
}
.buscador:focus-within {
box-shadow: 0 0 0 3px rgb(45 108 223 / 0.3);
}
:focus: el elemento tiene el foco, llegue como llegue (teclado, clic o JavaScript).:focus-visible: el navegador cree que conviene mostrar el foco. Con teclado, siempre; al hacer clic en un botón con el ratón, no. Es la que debes usar para los anillos de foco: quien navega con teclado los ve y a quien usa ratón no le molestan.:focus-within: el elemento contiene algo con el foco. Perfecto para resaltar la caja entera de un buscador o de un grupo de campos.
Cuidado: nunca pongas
outline: noneen:focussin dar otro estilo de foco visible. Dejarías a quien usa teclado sin saber dónde está.
:link y :visited
:link son los enlaces aún no visitados; :visited, los que ya visitaste. Por privacidad (para que una web no pueda averiguar tu historial), :visited solo permite cambiar colores: color, background-color, border-color y poco más.
Si estilizas los cuatro estados de un enlace, respeta este orden, porque todos tienen la misma especificidad y gana el último:
a:link { color: #2d6cdf; }
a:visited { color: #6f42c1; }
a:hover { color: #e63946; }
a:active { color: #b02a37; }
Una regla para recordarlo: LoVe HAte (link, visited, hover, active).
Estados de formularios
Los campos de formulario tienen sus propias pseudoclases. Las verás en acción en la lección de formularios con CSS; aquí tienes el mapa completo.
| Pseudoclase | Selecciona |
|---|---|
:checked | Casillas y botones de opción marcados, y la <option> elegida |
:disabled / :enabled | Campos desactivados o activos |
:required / :optional | Campos con o sin el atributo required |
:valid / :invalid | Campos que cumplen o no su validación |
:user-valid / :user-invalid | Lo mismo, pero solo después de que el usuario haya interactuado |
:placeholder-shown | Campos que están mostrando su texto de ejemplo (están vacíos) |
:read-only | Campos con readonly |
:in-range / :out-of-range | Números o fechas dentro o fuera de min y max |
input:disabled { opacity: 0.5; cursor: not-allowed; }
label:has(+ input:required)::after {
content: " *";
color: #c0392b;
}
input[type="checkbox"]:checked + label {
text-decoration: line-through; /* tarea de la lista hecha */
}
:invalid frente a :user-invalid
:invalid tiene un problema: un campo required vacío ya es inválido nada más cargar la página, así que el formulario aparece lleno de rojo antes de que la persona escriba nada. :user-invalid espera a que el usuario haya tocado el campo (haya escrito y salido de él, o haya intentado enviar):
input:user-invalid {
border-color: #c0392b;
background: #fdecea;
}
input:user-valid {
border-color: #1e7e34;
}
Funciona en todos los navegadores actuales desde finales de 2023. En código antiguo verás trucos como input:not(:placeholder-shown):invalid, que hacía algo parecido.
:placeholder-shown
Se cumple mientras el campo muestra su placeholder, es decir, mientras está vacío. Se usa para las etiquetas flotantes: la etiqueta está dentro del campo y sube cuando escribes.
.campo input:not(:placeholder-shown) + label {
transform: translateY(-1.4em) scale(0.8);
}
Para que funcione, el campo necesita un placeholder, aunque sea un espacio: <input placeholder=" ">.
Pseudoclases estructurales: la posición
Seleccionan un elemento según el lugar que ocupa entre sus hermanos. No hace falta poner clases como primero o par en el HTML.
li:first-child { border-top: none; } /* el primer hijo */
li:last-child { border-bottom: none; } /* el último hijo */
li:only-child { list-style: none; } /* el único hijo de su padre */
Ojo con un detalle: p:first-child significa “un <p> que además es el primer hijo de su padre”. Si el primer hijo es un <h2>, ningún párrafo cumple la regla.
:nth-child(): la fórmula an+b
:nth-child() acepta un número, las palabras odd (impares) y even (pares), o una fórmula an+b. El navegador va dando a n los valores 0, 1, 2, 3… y selecciona los hijos cuya posición sale en el resultado (las posiciones empiezan en 1).
| Selector | Posiciones | Para qué |
|---|---|---|
:nth-child(3) | 3 | Solo el tercero |
:nth-child(odd) o (2n+1) | 1, 3, 5, 7… | Filas impares |
:nth-child(even) o (2n) | 2, 4, 6, 8… | Filas pares |
:nth-child(3n) | 3, 6, 9… | Uno de cada tres |
:nth-child(3n+1) | 1, 4, 7, 10… | El primero de cada fila de tres |
:nth-child(n+4) | 4, 5, 6… | Del cuarto en adelante |
:nth-child(-n+3) | 3, 2, 1 | Los tres primeros |
:nth-child(n+2):nth-child(-n+5) | 2, 3, 4, 5 | Un rango: del segundo al quinto |
Compruébalo a mano con -n+3: con n = 0 sale 3; con n = 1, 2; con n = 2, 1; con n = 3, 0, que no es una posición válida. Por eso solo selecciona los tres primeros.
Un ejemplo real: la clasificación de una carrera popular, con los tres primeros en negrita y el resto en filas alternas.
.clasificacion tr:nth-child(-n+3) { font-weight: bold; }
.clasificacion tr:nth-child(even) { background: #f1f4f9; }
Y en una galería de tres columnas, quitar el margen derecho de la última foto de cada fila:
.galeria img:nth-child(3n) { margin-right: 0; }
Contar desde el final: :nth-last-child()
Funciona igual, pero cuenta desde el último:
li:nth-last-child(2) { color: gray; } /* el penúltimo */
li:nth-last-child(-n+3) { opacity: 0.6; } /* los tres últimos */
Filtrar con of
Desde 2023, :nth-child() permite contar solo los hermanos que cumplen un selector:
/* el segundo de los elementos con clase .destacado, ignorando los demás */
.producto:nth-child(2 of .destacado) { outline: 2px solid gold; }
:nth-of-type() y familia
:nth-child() cuenta todos los hermanos, sean de la etiqueta que sean. :nth-of-type() cuenta solo los de la misma etiqueta:
<article>
<h2>Ruta del Cares</h2>
<p>Primer párrafo.</p>
<img src="cares.jpg" alt="Desfiladero del Cares">
<p>Segundo párrafo.</p>
</article>
article p:nth-child(2) { color: blue; } /* el primer <p>: es el 2.º hijo */
article p:nth-of-type(2) { color: red; } /* el segundo <p>, aunque sea el 4.º hijo */
article p:first-of-type { font-size: 1.2em; }
Existen también :first-of-type, :last-of-type, :only-of-type y :nth-last-of-type().
:empty
Selecciona elementos sin ningún hijo, ni siquiera texto. Sirve para ocultar cajas que llegan vacías:
.mensaje:empty { display: none; }
Cuidado: un espacio o un salto de línea dentro de la etiqueta cuentan como contenido en los navegadores actuales, así que <div class="mensaje"> </div> no está vacío.
:not(), :is(), :where() y :has()
Estas cuatro pseudoclases reciben otros selectores dentro. Se explican con detalle, incluida su especificidad, en CSS moderno. En resumen:
li:not(:last-child) { border-bottom: 1px solid #ddd; } /* todos menos el último */
:is(h1, h2, h3):hover { color: #2d6cdf; } /* agrupa sin repetir */
:where(ul, ol) { padding-left: 1.2em; } /* como :is, sin especificidad */
.tarjeta:has(img) { padding: 0; } /* la tarjeta que contiene una imagen */
li:not(:last-child) es un clásico: separadores entre elementos sin que sobre uno al final.
Pseudoelementos
::before y ::after
Crean un hijo falso al principio o al final del contenido del elemento. Solo existen si tienen la propiedad content, aunque sea vacía:
.precio::after {
content: " €";
}
.nuevo::before {
content: "Nuevo";
margin-right: 0.5em;
padding: 2px 6px;
border-radius: 4px;
background: #e63946;
color: white;
font-size: 0.75em;
}
.decoracion::before {
content: ""; /* vacío: solo sirve como caja de adorno */
display: block;
width: 40px;
height: 4px;
background: #2d6cdf;
}
content admite texto, "", el valor de un atributo con attr() y contadores:
a[href^="http"]::after { content: " (" attr(href) ")"; } /* útil al imprimir */
Dos límites importantes:
- No funcionan en elementos que no tienen contenido propio, como
<img>,<input>o<br>. - El texto de
contentno se puede seleccionar y los lectores de pantalla no siempre lo leen. Úsalo para adornos, nunca para información que el usuario necesite.
::first-letter y ::first-line
Estilizan la primera letra y la primera línea de un bloque de texto. La primera línea depende del ancho de la pantalla, y el navegador la recalcula sola:
.articulo p:first-of-type::first-letter {
float: left;
font-size: 3.2em;
line-height: 1;
margin-right: 0.1em;
color: #2d6cdf; /* letra capital, como en los libros */
}
.articulo p:first-of-type::first-line {
font-variant: small-caps;
}
::marker
Es la viñeta o el número de cada <li> (y el triángulo de <summary>). Solo admite unas pocas propiedades: color, fuente y content.
.ingredientes li::marker { color: #e67e22; }
.pasos li::marker { content: "Paso " counter(list-item) ": "; font-weight: bold; }
Nota: Safari todavía ignora
contenten::marker; ahí se verá el número normal. El color sí funciona en todos.
::selection
El texto que el usuario selecciona con el ratón. Solo admite colores y poco más:
::selection { background: #ffd166; color: #1b2d34; }
::placeholder
El texto de ejemplo de un campo. Muchos navegadores lo ponen demasiado claro: asegúrate de que tenga contraste suficiente.
input::placeholder { color: #6b7280; font-style: italic; }
::backdrop
Es la capa que hay detrás de un <dialog> abierto con showModal() o de un elemento a pantalla completa. Permite oscurecer el resto de la página:
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
}
Lo usarás con la lección de details y dialog.
Errores frecuentes
- Olvidar
contenten::beforeo::after. Sincontentel pseudoelemento no existe, aunque le pongas tamaño y color. Si es solo decorativo,content: "". - Un solo par de dos puntos en pseudoelementos.
p:beforefunciona por compatibilidad con CSS antiguo, pero la forma correcta esp::before. Las pseudoclases llevan uno; los pseudoelementos, dos. - Pensar que
:nth-childcuenta solo los de esa etiqueta.p:nth-child(2)es “el segundo hijo, si es un<p>”. Para contar solo párrafos,:nth-of-type. - Poner
::aftera una imagen o a uninput. No tienen contenido donde insertarlo. Envuélvelos en un<span>o<div>y ponlo en el envoltorio. - Usar
:invalidpara pintar errores. El formulario sale en rojo al cargar. Usa:user-invalid. - Desordenar los estados de los enlaces. Si
:hoverva antes que:visited, los enlaces visitados no cambiarán al pasar el ratón. - Quitar el foco sin alternativa. Usa
:focus-visiblepara un anillo claro en lugar de borrarlo.
Resumen
| Selector | Qué selecciona |
|---|---|
:hover / :active | Ratón encima / mientras se pulsa |
:focus-visible | Foco que conviene mostrar (teclado) |
:focus-within | Elemento que contiene algo con foco |
:checked, :disabled, :required | Estados de campos de formulario |
:user-invalid | Campo inválido tras interactuar con él |
:placeholder-shown | Campo vacío que muestra su placeholder |
:first-child, :last-child, :only-child | Posición entre hermanos |
:nth-child(an+b) | Posiciones según una fórmula |
:nth-of-type() | Posición contando solo la misma etiqueta |
:empty | Elemento sin hijos ni texto |
::before / ::after | Contenido decorativo creado con content |
::first-letter / ::first-line | Primera letra / primera línea |
::marker | Viñeta o número de una lista |
::selection, ::placeholder, ::backdrop | Texto seleccionado, texto de ejemplo, fondo de un modal |
Ya sabes seleccionar casi cualquier cosa. Pero ¿qué pasa cuando dos reglas apuntan al mismo elemento? Lo verás en cascada, especificidad y herencia.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.