Pseudoclases y pseudoelementos
¿Qué son?
Los selectores CSS vistos hasta ahora apuntan a elementos según su etiqueta, clase o id. Las pseudoclases y los pseudoelementos van más allá: permiten seleccionar elementos según su estado (el ratón está encima, el campo tiene el foco, el checkbox está marcado) o según partes virtuales de un elemento (la primera letra, el contenido generado antes o después).
La diferencia sintáctica es sencilla: las pseudoclases usan un solo dos puntos (:hover), los pseudoelementos usan dos (::before). La doble notación es la correcta en CSS3, aunque la simple también funciona por compatibilidad histórica.
Pseudoclases dinámicas
Responden a la interacción del usuario en tiempo real:
/* El ratón está encima del elemento */
button:hover {
background-color: #5a9e2f;
}
/* El elemento tiene el foco de teclado o clic */
input:focus {
outline: none;
border-color: #84ba3f;
box-shadow: 0 0 0 3px rgba(132, 186, 63, 0.25);
}
/* En el momento exacto del clic */
button:active {
transform: scale(0.98);
}
:focus es esencial para la accesibilidad: es la señal visual para usuarios que navegan con teclado. :focus-visible es una variante más refinada: solo aplica el estilo de foco cuando el navegador determina que es necesario (navegación por teclado), no cuando el usuario hace clic con el ratón:
/* Foco visible solo para teclado */
button:focus-visible {
outline: 2px solid #84ba3f;
outline-offset: 3px;
}
/* Eliminar el anillo feo del clic con ratón */
button:focus:not(:focus-visible) {
outline: none;
}
Pseudoclases de enlace
Se aplican exclusivamente a elementos <a> con atributo href. El orden importa (LoVe HAte):
a:link { color: #0066cc; } /* no visitado */
a:visited { color: #6600aa; } /* ya visitado */
a:hover { color: #003399; } /* ratón encima */
a:active { color: #cc0000; } /* en el clic */
Pseudoclases estructurales
Seleccionan elementos según su posición en el árbol HTML, sin necesidad de añadir clases:
/* Primer y último hijo */
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
/* El hijo en posición n (contando desde 1) */
tr:nth-child(2) { background: #f9f9f9; }
/* Pares e impares: patrón zebra */
tr:nth-child(even) { background: #f0f7e6; }
tr:nth-child(odd) { background: white; }
/* Fórmula: cada 3 elementos empezando por el primero */
li:nth-child(3n+1) { color: #84ba3f; }
/* El único hijo */
p:only-child { margin: 0; }
/* Seleccionar por tipo en lugar de por posición global */
p:first-of-type { font-size: 1.1rem; }
p:nth-of-type(2) { margin-top: 2rem; }
La diferencia entre :nth-child y :nth-of-type: el primero cuenta todos los hijos del contenedor; el segundo solo los del tipo especificado. Si mezclas <h2> y <p> en un contenedor, p:nth-child(2) solo aplica si el segundo hijo es un <p>, mientras que p:nth-of-type(2) aplica al segundo <p> independientemente de los otros elementos.
:not() invierte la selección:
/* Todos los botones excepto los desactivados */
button:not(:disabled) { cursor: pointer; }
/* Todos los li excepto el último (para bordes) */
li:not(:last-child) { border-bottom: 1px solid #eee; }
Pseudoclases de formulario
Los formularios tienen un conjunto rico de pseudoclases que reflejan el estado de cada control:
/* Campo desactivado */
input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
/* Campo obligatorio */
input:required {
border-left: 3px solid #cc3333;
}
/* Campo con dato válido / inválido */
input:valid { border-color: #44aa44; }
input:invalid { border-color: #cc3333; }
/* Checkbox o radio marcado */
input[type="checkbox"]:checked + label {
font-weight: bold;
}
/* Campo en solo lectura */
input:read-only { background: #fafafa; }
Pseudoclases modernas
:is() agrupa varios selectores en uno, reduciendo repetición. Su especificidad es la del selector más específico de la lista:
/* Sin :is() */
h1 a, h2 a, h3 a { color: inherit; }
/* Con :is() */
:is(h1, h2, h3) a { color: inherit; }
:where() funciona igual pero su especificidad es siempre cero, lo que facilita sobreescribirlo después:
:where(header, footer) a {
text-decoration: none; /* fácil de sobreescribir en cualquier contexto */
}
:has() es el "selector padre": selecciona un elemento en función de lo que contiene. Resuelve un problema que CSS nunca había podido solucionar directamente:
/* Tarjeta que contiene una imagen: más padding */
.tarjeta:has(img) { padding: 0; }
/* Label cuyo input asociado está marcado */
label:has(input:checked) { font-weight: bold; }
/* Formulario que tiene algún campo inválido */
form:has(:invalid) .btn-submit { opacity: 0.5; }
Nota: :has() tiene soporte en todos los navegadores modernos desde 2023, pero verifica la compatibilidad si necesitas dar soporte a versiones antiguas.
Pseudoelementos
Los pseudoelementos apuntan a partes de un elemento que no existen como nodos en el HTML:
/* Primera letra del primer párrafo */
article > p:first-of-type::first-letter {
font-size: 3em;
font-weight: bold;
float: left;
line-height: 1;
margin-right: 0.1em;
}
/* Primera línea de un párrafo */
p::first-line {
font-variant: small-caps;
}
/* Texto seleccionado por el usuario */
::selection {
background: #84ba3f;
color: white;
}
/* Placeholder de un input */
input::placeholder {
color: #aaa;
font-style: italic;
}
::before y ::after
::before y ::after insertan un nodo virtual antes o después del contenido del elemento. Requieren la propiedad content, que puede ser una cadena, una imagen, o vacía:
/* Icono antes de cada enlace externo */
a[href^="https://"]::after {
content: " ↗";
font-size: 0.75em;
opacity: 0.6;
}
/* Comillas decorativas */
blockquote::before { content: "\201C"; font-size: 3em; color: #ccc; }
blockquote::after { content: "\201D"; font-size: 3em; color: #ccc; }
/* Elemento de limpieza (clearfix moderno) */
.clearfix::after {
content: "";
display: block;
clear: both;
}
/* Contador automático de secciones */
body { counter-reset: seccion; }
h2::before {
counter-increment: seccion;
content: counter(seccion) ". ";
}
El contenido generado con ::before y ::after es invisible para los lectores de pantalla por defecto. Úsalo para decoración, no para información esencial.
La propiedad cursor
La propiedad cursor controla el aspecto del puntero cuando está sobre un elemento. Usarla correctamente refuerza la interfaz: el usuario sabe de un vistazo si algo es clicable, no disponible o arrastrable, sin necesidad de leer ninguna etiqueta.
Los valores más útiles en la práctica:
pointer: mano. Para cualquier elemento clicable que no sea un enlace nativo (botones, tarjetas, etiquetas clicables).default: flecha estándar. Útil para forzar el cursor por defecto donde el navegador elegiría otro automáticamente.not-allowed: círculo tachado. Para elementos deshabilitados o acciones bloqueadas.grabygrabbing: mano abierta y mano cerrada. Para elementos arrastrables, en reposo y durante el arrastre.zoom-inyzoom-out: lupa. Para imágenes o mapas ampliables.crosshair: cruz. En herramientas de selección o dibujo.wait: reloj o spinner. Para operaciones en curso sin posibilidad de interacción.text: cursor de texto. El navegador lo aplica automáticamente en zonas editables, pero a veces hace falta forzarlo.
button:disabled { cursor: not-allowed; }
.draggable { cursor: grab; }
.draggable:active { cursor: grabbing; }
.foto-ampliable { cursor: zoom-in; }
Se puede usar una imagen personalizada con url(). Siempre hay que incluir un valor de respaldo, porque si la imagen no carga el cursor desaparece:
canvas.herramienta {
cursor: url("/cursors/lapiz.png"), crosshair;
}
Mantén las imágenes por debajo de 32x32 px. Los formatos soportados son .cur, .png y .svg. Los cursores personalizados solo tienen sentido en interfaces muy concretas (editores gráficos, juegos); en una web convencional son más distracción que ayuda.
Nota: En pantallas táctiles no hay cursor visible, así que cursor no tiene efecto en móviles. Para dar retroalimentación en táctil usa cambios de color o escala con :active.
Recapitulación
- Las pseudoclases (
:) seleccionan según estado o posición. Los pseudoelementos (::) apuntan a partes virtuales del elemento. - Pseudoclases dinámicas:
:hover,:focus,:active. Usar:focus-visiblepara estilizar el foco solo cuando es necesario. - Pseudoclases de enlace:
:link,:visited,:hover,:active. El orden LoVe HAte importa. - Pseudoclases estructurales:
:first-child,:last-child,:nth-child(),:nth-of-type(),:not(). - Pseudoclases de formulario:
:disabled,:required,:valid,:invalid,:checked. :is()agrupa selectores (hereda especificidad).:where()hace lo mismo con especificidad cero.:has()selecciona por contenido.::beforey::afterinsertan nodos virtuales. Requierencontent. Son invisibles para lectores de pantalla.::first-letter,::first-line,::selection,::placeholder,::marker.cursorcontrola el puntero sobre el elemento:pointerpara clicables,not-allowedpara deshabilitados,grab/grabbingpara arrastrables. No tiene efecto en pantallas táctiles.
Tu proyecto
Añade estados visuales a los elementos interactivos del portfolio. Todos estos estilos van en estilos.css:
/* Orden LoVe HAte para los enlaces de nav */
.nav-principal a:link { color: inherit; }
.nav-principal a:visited { color: inherit; }
.nav-principal a:hover { color: #84ba3f; }
.nav-principal a:active { opacity: 0.7; }
/* Indicador de foco accesible para teclado */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 2px solid #84ba3f;
outline-offset: 3px;
}
/* Efecto hover en las tarjetas de proyecto */
.tarjeta:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
/* Resaltado de texto seleccionado */
::selection {
background: #84ba3f;
color: white;
}
Pasa el cursor sobre los enlaces de navegación y comprueba el cambio de color. Selecciona un párrafo de texto y observa el color personalizado.
En la próxima lección: variables CSS: cómo centralizar colores, tamaños y otros valores para mantener el diseño coherente sin repetición.