Pseudoelemento - Imagen conceptual

CSS: Interactividad y estilo con pseudoclases y pseudoelementos

Eres responsable de cómo reacciona tu interfaz cuando alguien la toca.
No hablo de colores bonitos. Hablo de intención. De gramática. De cómo un :focus-visible dice “aquí puedes actuar” y cómo un ::before susurra contexto sin contaminar el contenido.
Si usas pseudoclases y pseudoelementos como maquillaje, fabricas ruido. Si los usas como estrategia, fabricas criterio.

Por qué demonios hablamos de esto

Tus usuarios no leen tu CSS: lo sienten.
Lo sienten cuando un formulario avisa sin humillar, cuando un enlace confirma que es clicable, cuando una lista tiene ritmo, cuando la selección no secuestra el contraste.
Este manifiesto es para desmontar dogmas, instalar un modelo mental y dejar reglas operativas que mañana puedas auditar sin pedirme permiso.

Acto I — La mentira de la medianía

Dogma reproducido:
“Las pseudoclases y los pseudoelementos son azúcar sintáctico. Con clases y dos !important tiro.”

Autopsia (síntoma → causa raíz → cadena de decisiones):
Tienda con 3 equipos. Cada uno “resuelve” estados con utilidades y !important.
Síntoma: el botón “Comprar” cambia de aspecto según la página; los inputs marcan error con color pero sin foco; el subrayado de enlaces baila.
Causa raíz: ausencia de doctrina de estados; desconocimiento de :focus-visible, :invalid, :has(), ::before/::after.
Cadena: parche local → incoherencia global → fricción → caída en conversión.

Evidencia mínima (coste):
+12 KB de CSS redundante, 17 !important, 3 h de QA por sprint en “estados”, p95 INP empeora 12 ms por animaciones inútiles.

Smells (3 señales de pantano):

  • Estados definidos por componente sin tokens ni reglas comunes
  • Foco invisible o idéntico al estado hover (teclado castigado)
  • Decoración metida en HTML en vez de ::before/::after

Contraejemplo rápido (antes/después):

Antes — hover y foco mezclados, especificidad alta:

CSS

.header .nav .item a:hover { color: #0af; }
.header .nav .item a:focus { outline: none; color: #0af; }

Después — intención separada, especificidad domesticada:

CSS

/* 2025: bajar especificidad con :where() y distinguir foco real */
:where(a):hover { color: var(--link-hover); }
:where(a):focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

Regla de salida del Acto I:
Si te reconoces en esos smells, estás perdiendo tiempo y dinero. La estética es el síntoma; el coste es real.

Acto II — La verdad (doctrina y cómo comprobable)

Visión:
Pseudoclases = estados de comportamiento; pseudoelementos = decoración semánticamente separada. Diseñas la cadena de mando de la interacción: intención → selección → estado → feedback → siguiente acción.

Modelo operativo (4–6 nodos, verbal):

  • Origen: puntero/teclado/táctil
  • Selección: :is()/:where()/:not() para agrupar sin subir especificidad
  • Detección: :hover, :active, :focus-visible, :target, :checked, :invalid, :disabled, :has()
  • Composición: feedback visual y decoraciones con ::before, ::after, ::marker, ::selection, ::placeholder
  • Reglas: tokens, tiempos, límites (propiedades animables y duración)
  • Salida: accesible, consistente, medible

Tácticas de campo (qué, por qué, y cuándo no)

1) Estados interactivos unificados
Qué: define tokens y tiempos; distingue :hover (apuntamiento) de :focus-visible (navegación) y :active (activación).
Por qué: reduces ambigüedad y deuda.
No usar: no apliques :hover a elementos no clicables.

CSS

/* tokens */
:root{ --t-200: .2s ease; --c-link: #2563eb; --c-link-h: #1d4ed8; }
:where(a){ color:var(--c-link); transition:color var(--t-200); }
:where(a):hover{ color:var(--c-link-h); }
:where(a):focus-visible{ outline:2px solid currentColor; outline-offset:2px; }

Ver ejemplo: hover vs focus-visible

2) Validación honesta con :required/:invalid/:has()
Qué: usa :invalid y :has() para iluminar el grupo, no humillar al usuario.
Por qué: accesibilidad y velocidad de corrección.
No usar: no bloquees el envío con solo color; añade mensajes y aria.

CSS
/* resalta el campo y su etiqueta si falla */
.field:has(input:invalid) label{ color:#b91c1c; }
input:invalid{ border-color:#b91c1c; outline:none; }
input:invalid:focus-visible{ outline:2px solid #b91c1c; outline-offset:2px; }

Ver ejemplo: validación con :invalid + :has()

3) Decoración sin contaminar HTML con ::before/::after
Qué: iconos, subrayados, badges, flechas… como decoración, no contenido.
Por qué: mantienes semántica limpia y controlas especificidad.
No usar: no metas contenido significativo (lectores de pantalla lo ignorarán).

CSS
/* subrayado animado sin tocar el HTML */
.link-underline{ position:relative; }
.link-underline::after{
  content:""; position:absolute; left:0; bottom:-2px;
  height:2px; width:0%; background:currentColor; transition:width .2s ease;
}
.link-underline:hover::after{ width:100%; }

Ver ejemplo: ::before/::after al servicio de la intención

4) Agrupar sin subir especificidad con :is()/:where() y excluir con :not()
Qué: escribe reglas claras para familias de elementos.
Por qué: un CSS que escala sin lucha interna.
No usar: evita selectores crípticos; documenta.

CSS
/* familia de acciones con misma pauta de foco */
:is(button,.btn,a[role="button"]):focus-visible{ outline:2px solid currentColor; }
/* baja especificidad de base, eleva en componentes si hace falta */
:where(h1,h2,h3){ scroll-margin-top:6rem; }

Ver ejemplo: agrupar con :is() y :where()

Demostración mínima (micro-experimento replicable en 10 minutos):

  1. Crea una página con un enlace, un botón y un input required.
  2. Aplica la táctica 1 (tokens + hover/focus-visible), la 2 (invalid + has) y la 3 (subrayado con ::after).
  3. Navega con teclado: el foco debe ser visible; fuerza un error de validación; verifica que el grupo reacciona.
  4. Mide: 0 !important, duración ≤ 300 ms, especificidad de reglas base ≤ 0-1-1.
  5. Resultado esperado: coherencia de estados, foco inequívoco, HTML limpio.

Trade-offs (costes aceptados):

  • :has() requiere soporte moderno: define fallback sin él
  • Más disciplina: separar contenido y decoración
  • Documentación mínima de tokens y estados

Impacto en negocio (tangible):

  • Time-to-change global de estados < 10 minutos
  • −20% CSS de estados en dos sprints
  • Menos errores de formulario → más completados

Regla de salida del Acto II:
Si un junior sigue las tácticas y la demo, reproduce el resultado hoy. Si no, era opinión, no doctrina.

Acto III — El manifiesto (ley operativa)

Principios no negociables (8–12):

  • Separa intención (pseudoclases) de decoración (pseudoelementos)
  • Define tokens de estado y tiempos globales
  • El foco se ve siempre; el hover jamás lo sustituye
  • No más de 2 propiedades animadas, ≤ 300 ms
  • Preserva semántica: nada significativo en ::before/::after
  • Orquesta familias con :is()/:where() y reduce especificidad
  • Usa :has() para feedback contextual, con fallback
  • 0 !important en estados
  • Documenta “cuándo no” tanto como “cuándo sí”
  • Audita con KPIs en cada PR

Definición de hecho (DoD):

  • Todos los elementos interactivos tienen :hover/:focus-visible coherentes
  • Formularios con :invalid/:disabled y mensaje accesible
  • Decoración fuera del HTML mediante ::before/::after
  • Especificidad media de estados ≤ 0-1-1
  • 0 !important, 0 focos invisibles

Métricas de guardarraíl (KPIs):

  • Duración media de estado ≤ 250–300 ms
  • % reglas con :where()/:is() en base ≥ 60%
  • Bytes de CSS de estados ≤ 15% del total
  • p95 INP no empeora por animaciones de estado

Coto de caza (anti-patrones prohibidos):

  • Foco eliminado con outline: none sin alternativa visible
  • Hover en elementos no accionables
  • Decoración significativa metida en HTML
  • Estados distintos por sección sin tokens
  • Animaciones que cambian layout (saltos de caja)

Plan de acción de 24h (primeros 5 pasos):

  • Crear tokens de estados y tiempos en :root
  • Auditar y sustituir !important en estados
  • Instalar :focus-visible en acciones y enlaces
  • Migrar subrayados/ornamentos a ::before/::after
  • Documentar “familias” con :is()/:where() y publicar guía

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *