Saltar al contenido
pseudoclases.css · devschool

Pseudoclases y pseudoelementos en CSS

Lección 3 de 50 · 12 min de lectura · Actualizado el

En esta lección
  1. Pseudoclase o pseudoelemento
  2. Estados de interacción
  3. Estados de formularios
  4. Pseudoclases estructurales: la posición
  5. :not(), :is(), :where() y :has()
  6. Pseudoelementos
  7. Errores frecuentes
  8. Resumen

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: none en :focus sin dar otro estilo de foco visible. Dejarías a quien usa teclado sin saber dónde está.

: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.

PseudoclaseSelecciona
:checkedCasillas y botones de opción marcados, y la <option> elegida
:disabled / :enabledCampos desactivados o activos
:required / :optionalCampos con o sin el atributo required
:valid / :invalidCampos que cumplen o no su validación
:user-valid / :user-invalidLo mismo, pero solo después de que el usuario haya interactuado
:placeholder-shownCampos que están mostrando su texto de ejemplo (están vacíos)
:read-onlyCampos con readonly
:in-range / :out-of-rangeNú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).

SelectorPosicionesPara qué
:nth-child(3)3Solo 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, 1Los tres primeros
:nth-child(n+2):nth-child(-n+5)2, 3, 4, 5Un 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 content no 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 content en ::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 content en ::before o ::after. Sin content el pseudoelemento no existe, aunque le pongas tamaño y color. Si es solo decorativo, content: "".
  • Un solo par de dos puntos en pseudoelementos. p:before funciona por compatibilidad con CSS antiguo, pero la forma correcta es p::before. Las pseudoclases llevan uno; los pseudoelementos, dos.
  • Pensar que :nth-child cuenta 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 ::after a una imagen o a un input. No tienen contenido donde insertarlo. Envuélvelos en un <span> o <div> y ponlo en el envoltorio.
  • Usar :invalid para pintar errores. El formulario sale en rojo al cargar. Usa :user-invalid.
  • Desordenar los estados de los enlaces. Si :hover va antes que :visited, los enlaces visitados no cambiarán al pasar el ratón.
  • Quitar el foco sin alternativa. Usa :focus-visible para un anillo claro en lugar de borrarlo.

Resumen

SelectorQué selecciona
:hover / :activeRatón encima / mientras se pulsa
:focus-visibleFoco que conviene mostrar (teclado)
:focus-withinElemento que contiene algo con foco
:checked, :disabled, :requiredEstados de campos de formulario
:user-invalidCampo inválido tras interactuar con él
:placeholder-shownCampo vacío que muestra su placeholder
:first-child, :last-child, :only-childPosición entre hermanos
:nth-child(an+b)Posiciones según una fórmula
:nth-of-type()Posición contando solo la misma etiqueta
:emptyElemento sin hijos ni texto
::before / ::afterContenido decorativo creado con content
::first-letter / ::first-linePrimera letra / primera línea
::markerViñeta o número de una lista
::selection, ::placeholder, ::backdropTexto 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).

pseudoclases.html
Resultado

Pon a prueba lo que has aprendido

[CSS] ¿Qué selecciona li:nth-child(odd)?

[CSS] ¿Qué le pasa a div::before { width: 20px; height: 20px; background: red; } si no tiene content?

[CSS] ¿Por qué se usa :user-invalid en vez de :invalid para pintar errores?

[CSS] ¿Qué diferencia hay entre p:nth-child(2) y p:nth-of-type(2)?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.