Estados hover y pulsado: derivarlos de la escala
El color hover de un botón no debería calcularse a partir de su color en reposo: debería ser el paso vecino en la misma escala de color. En un tema claro, el hover es un paso más oscuro que el reposo y el pulsado es un paso más oscuro todavía: un botón que reposa en brand-600 hace hover en brand-700 y se pulsa en brand-800. Tres colores reales de la rampa, no dos variantes filtradas de uno solo.
La regla suena casi demasiado pequeña como para merecer un artículo y, sin
embargo, su inversa —estados hover producidos con opacidad, darken() o un
filtro de brillo— está entre los defectos más comunes de las interfaces
publicadas. Por qué ganan los pasos vecinos, a qué distancia deben quedar y
por qué toda la regla invierte su dirección en modo oscuro se desprende de
cómo se construyen las escalas de color, que es el terreno que cubre nuestra
guía de escalas de color.
¿Por qué no usar opacidad o un filtro darken()?
Porque los filtros abandonan la rampa. El hover de brand-600 se convierte en un color que no existe en ningún otro lugar del sistema —sin nombre, sin probar, invisible para el pipeline de tokens— y se comporta mal de tres maneras concretas.
La opacidad se mezcla con lo que haya detrás. Un botón al 85 % de opacidad muestra un color sobre una página blanca y otro distinto sobre una tarjeta con tinte, así que el “mismo” estado hover varía por todo el producto. Peor aún, la mezcla puede chocar con el fondo: un azul translúcido sobre una superficie cálida absorbe esa calidez, y nadie eligió jamás ese resultado.
El oscurecimiento por canales deriva. darken() y sus parientes reducen
los valores RGB y, en sRGB, esa operación hace algo más que oscurecer:
normalmente desatura y puede desplazar el tono a medida que los canales se
recortan a ritmos distintos. El hover sale más grisáceo y con el tono
ligeramente desviado, percibido como sucio en lugar de más profundo.
El color filtrado nunca pasó una comprobación de contraste. El color en reposo pasó sus mínimos; la salida del filtro no pasó nada. Un texto que cumplía 4,5:1 en reposo puede fallar sin avisar al hacer hover.
La mecánica de CSS es la parte trivial —
:hover
intercambia una propiedad personalizada o una clase. La pregunta siempre fue
únicamente qué color trae ese intercambio, y la escala ya contiene la
respuesta correcta: el paso de al
lado, generado por las mismas reglas, en el mismo tono, ya comprobado.
¿Qué tan grande debería ser el salto entre estados?
Un paso suele ser lo correcto, y los límites de ambos lados explican por qué.
El suelo es perceptual: un cambio por debajo de la diferencia apenas perceptible se lee como ningún cambio. Scale Composer avisa cuando dos pasos adyacentes quedan a menos de 0,02 L uno del otro, porque un salto de hover más pequeño que eso es un hover que la gente no puede ver: el botón parece muerto aunque la hoja de estilos jure que responde.
El techo es la identidad: un salto de varios pasos se lee como un color distinto en lugar de un estado distinto. El botón parece cambiar de idea sobre lo que es, y el ojo interpreta un reemplazo, no una pulsación. Entre esos límites, un paso bien construido cae con comodidad: claramente visible y, aun así, inconfundiblemente el mismo color. El pulsado toma entonces el paso siguiente al hover: la interacción se profundiza en la misma dirección en la que empezó.
¿Cómo se ve el recorrido en números?
A partir del color semilla azul #2563eb — oklch(0.546 0.215 262.9) — el
vecindario 600/700/800 de la
rampa queda así:
| Estado | Paso | OKLCH | Hex |
|---|---|---|---|
| reposo | 600 | oklch(0.52 0.134 262.9) | ≈#3E65B5 |
| hover | 700 | oklch(0.43 0.073 262.9) | ≈#3A4F78 |
| pulsado | 800 | oklch(0.34 0.031 262.9) | ≈#2F3848 |
Cada movimiento baja la luminosidad en ≈0,09 —unas cuatro veces el umbral
JND de 0,02, así que inconfundible— mientras el tono se mantiene en 262,9° a
lo largo de los tres pasos. Esa columna de tono es el argumento silencioso
contra los filtros: un darken() en sRGB no lo habría conservado. El croma
que se suaviza hacia el extremo oscuro es el propio plan de saturación de la
rampa en acción, por lo que el estado pulsado se lee como el mismo azul
pulsado más a fondo en vez de un color nuevo y más grisáceo. Scale Composer
deriva stateNormal, stateHover y statePressed entre sus roles
semánticos exactamente así: pasos vecinos en la rampa, asignados contra los
mínimos de contraste de la superficie.
Abre este recorrido de tres pasos en Scale Composer: reposo, hover y pulsado resaltados como vecinos en la rampa azul, con las diferencias de luminosidad entre ellos medidas.

¿Y el foco y el estado deshabilitado?
Dos estados quedan fuera de la regla del recorrido, brevemente.
El foco no es un cambio de relleno. Es un rol de anillo aparte: un contorno que debe seguir siendo visible tanto contra el botón como contra la página que hay detrás. Derivarlo como “un paso más” lo entierra; quien navega con teclado necesita que el anillo se anuncie, no que se funda con la secuencia de pulsación.
El estado deshabilitado se sale por completo de la lógica de interacción. Es la pareja de croma bajo y contraste bajo del conjunto de roles, y a propósito: un control deshabilitado comunica retrayéndose. Los mínimos de contraste no se le aplican —las pautas de accesibilidad eximen a los controles inactivos— y por eso es el único estado que se supone que debe fallar las comprobaciones que los demás pasan.
¿Por qué el modo oscuro invierte la dirección?
En las superficies oscuras, el hover va más claro, no más oscuro. La convención viene de la lógica de elevación: en los temas oscuros, las superficies más cercanas al espectador se renderizan más claras, y un control con hover está momentáneamente “elevado”. También hay una razón puramente práctica: desde un paso de reposo oscuro, ir más oscuro se dirige hacia el negro, y el cambio de estado se ahoga.
Una paleta oscura derivada consigue la inversión por construcción y no por excepción. Scale Composer deriva las paletas oscuras a partir del mismo color semilla con su propia curva de luminosidad y un aumento de croma de en torno al 20 % (el entorno oscuro atenúa el colorido percibido), de modo que los pasos de la rampa oscura son colores reales y ordenados por sí mismos, y “avanzar un paso en la dirección interactiva” simplemente apunta hacia el otro lado a lo largo de ellos. Sin trucos de hover específicos por tema; la regla sobrevive, la dirección se invierte.
Cambia el mismo botón a la paleta oscura derivada: los roles de reposo, hover y pulsado vueltos a derivar en la rampa oscura, avanzando más claro donde el tema claro avanzaba más oscuro.