Hex a HSL: cuándo HSL ayuda y el algoritmo en palabras
HSL es el mismo color que el hex pero rotado a un sistema de coordenadas pensado para humanos. El motivo de convertir es casi siempre el theming. Con HSL puedes sacar variantes hover, disabled y accesibles de un color de marca con aritmética en vez de a ojo.
Por qué HSL compensa
El hex te dice los valores byte de un color. El HSL te dice cómo lo describiría una persona: un tono (dónde en la rueda de color), una saturación (cuánto vibra), y una luminosidad (cuán cerca de blanco o negro está). Para un diseñador eso mapea con la intuición, para un desarrollador mapea con matemáticas.
El día que escribas un botón con hover cinco por ciento más oscuro, HSL ya se paga solo. Quitas cinco puntos a la lightness y tienes el hover. Quitas diez y tienes el active. Bajas la saturación veinte y tienes el disabled. Hacer lo mismo con hex son tres viajes a un color picker y un diseñador que decide cada paso a ojo.
Los sistemas de theming se apoyan en HSL por la misma razón. Las custom properties de CSS guardan tono y saturación, la lightness varía para la escala de tonos. Tailwind v4 prefiere OKLCH por uniformidad perceptual, pero HSL sigue en todas partes y es el salto más pequeño desde hex.
El algoritmo en palabras
Coge el hex, convierte cada canal a un float de 0 a 1 (ff se vuelve 1.0, 88 se vuelve 0.533). Localiza el canal mayor y el menor. La lightness es la media de los dos: L = (max + min) / 2.
Si max es igual a min el color es gris, tono y saturación son cero, has acabado. Si no, la saturación depende de la lightness. Por debajo de 0.5 la saturación es (max - min) / (max + min), en o por encima de 0.5 es (max - min) / (2 - max - min). El corte es feo pero sale de la geometría, no le des más vueltas.
El tono lo decide qué canal es el max. Si rojo es el mayor, el tono es ((g - b) / (max - min)) * 60, con un wrap para que quede positivo. Si verde es el mayor, ((b - r) / (max - min) + 2) * 60. Si azul es el mayor, ((r - g) / (max - min) + 4) * 60. Tres casos, cada uno rota la rueda 120 grados.
El resultado es un tono de 0 a 360, saturación y luminosidad de 0 a 1. Multiplica por 100 para tener los porcentajes que CSS espera.
CSS hsl() y alpha
El CSS moderno acepta tanto hsl(35, 100%, 50%) (forma legacy con comas) como hsl(35 100% 50%) (forma moderna con espacios). La forma con espacios compone con alpha como hsl(35 100% 50% / 0.5), que es la sintaxis que quieres en código nuevo.
hsla() sigue por compatibilidad, pero el hsl() moderno acepta el alpha con barra y es la opción preferida. Si tu tooling se queja, es que es más viejo que 2022. Actualiza.
El tono acepta sufijo de unidad. 35deg y 35 son lo mismo. Puedes usar turn (0.097turn), grad (38.9grad) o rad (0.611rad) cuando la cuenta salga más fácil en esas unidades. La mayoría del código se queda en grados porque cualquier otra herramienta habla grados.
Uso de diseñador y desarrollador
En la práctica la conversión va en una dirección (hex entra, HSL sale) y el consumidor es una hoja de estilos que guarda tono y saturación como design tokens. La lightness es la variable. --brand: 217 91% 60%; vive en el root, y el botón usa hsl(var(--brand)) para la base, hsl(var(--brand) / 0.85) para el hover, hsl(217 91% 50%) para el active.
El snippet de abajo convierte un hex al trío HSL. Mételo en un paso de build, vuelca los tokens una vez, y el resto de la hoja se escribe sola.
Ejemplo completo
javascript// Hex a HSL. Devuelve { h: 0..360, s: 0..100, l: 0..100, a: 0..1 }.
function hexToHsl(hex) {
const m = hex.trim().replace(/^#/, "").toLowerCase();
const full =
m.length === 3 || m.length === 4 ? m.split("").map((c) => c + c).join("") : m;
if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/.test(full)) return null;
const r = parseInt(full.slice(0, 2), 16) / 255;
const g = parseInt(full.slice(2, 4), 16) / 255;
const b = parseInt(full.slice(4, 6), 16) / 255;
const a = full.length === 8 ? parseInt(full.slice(6, 8), 16) / 255 : 1;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0;
let s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
else if (max === g) h = ((b - r) / d + 2) * 60;
else h = ((r - g) / d + 4) * 60;
}
return {
h: Math.round(h),
s: Math.round(s * 100),
l: Math.round(l * 100),
a: Math.round(a * 1000) / 1000,
};
}
// hexToHsl("#3a86ff") -> { h: 217, s: 100, l: 61, a: 1 }
// Como CSS: -> hsl(217 100% 61%) ¿Solo necesitas el resultado?
Pega el hex en el convertidor de color en aldeacode.com y el HSL, OKLCH y HSV salen a la vez con la sintaxis CSS correcta. Copias el design token directo, sin matemáticas con floats en la cabeza, sin un grado de menos en la rotación del tono.
Abrir Convertidor de Color (Hex / RGB / HSL) →Preguntas frecuentes
¿Por qué mi HSL convertido no coincide exactamente con el de mi diseñador?
Cada herramienta redondea en sitios distintos. Algunas redondean los floats antes de la saturación, otras después. Diferencias de uno o dos en tono o saturación son normales y visualmente idénticas. Elige una fuente única (tu build, el archivo de diseño o el conversor) y deja de comparar.
¿Uso HSL u OKLCH para theming?
OKLCH es uniforme perceptualmente, así un descenso de 10 puntos en lightness se ve igual de oscuro en todos los tonos. HSL no, así que un amarillo al 50 por ciento parece más claro que un azul al 50 por ciento. Para sistemas nuevos OKLCH gana. Para CSS existente que ya habla HSL, la migración rara vez compensa por sí sola.
¿Puedo guardar el trío HSL sin el wrapper hsl()?
Sí. Las custom properties de CSS aceptan valores crudos, así --brand: 217 91% 60% funciona si lo envuelves en el sitio de uso: hsl(var(--brand)). Este patrón te deja meter alpha en el sitio de uso en vez de en la definición.