Módulo 4: Scales Color And Contrast

Paletas de color y steps

Descripción

El color no escala como el espacio o el texto. Un espacio tiene un solo eje (más grande o más chico) y una tipografía también (más grande o más chica), así que una escala de un solo número les basta. El color tiene dos ejes: el tono (¿es azul, gris, rojo?) y la claridad (¿es un azul casi blanco o casi negro?). Por eso una escala de color no es una línea sino una rampa: un tono fijo, recorrido en pasos de claridad. Esos pasos son los steps, y la convención —la de Tailwind, Material, Radix— los numera de 50 (el más claro) a 900 (el más oscuro). En esta lección construyes la anatomía de una rampa, ves por qué el step numera la claridad (no el tono), y preparas el terreno para el contraste: medir el contraste entre dos colores es, en el fondo, medir la distancia entre sus claridades.

Conexión con el módulo. Es la tercera escala del módulo —después del espaciado (L2) y la tipografía (L3)—, y la más rica, porque introduce el eje de la claridad que las lecciones 5, 6 y 7 medirán como contraste. Es el valor detrás de tus tokens de color del módulo 2: blue.600 (#2563eb) era un step de una rampa; aquí ves la rampa completa de la que salió. Y las rampas que definas aquí son la materia prima del proyecto (L8), donde eliges qué steps usar para cada rol y auditas su contraste.

Una analogía: la escala de grises de un lápiz, en todos los colores

Piensa en cómo un artista organiza sus lápices de un mismo color. No tiene "un azul": tiene una serie de azules, del más claro (casi blanco, para cielos lejanos) al más oscuro (casi negro, para sombras profundas), con peldaños regulares en medio. Los ordena en una tira, del claro al oscuro, y cuando necesita "un azul un poco más oscuro" toma el lápiz de al lado. La tira es una rampa: un tono (azul) recorrido en pasos de claridad. Y tiene una tira así por cada tono que usa —una de grises, una de azules, una de rojos—.

Una paleta de sistema es exactamente ese juego de tiras. Cada rampa es un tono en pasos de claridad, y los pasos se numeran para poder pedirlos por nombre: blue.50 es el azul casi blanco (el extremo claro de la tira), blue.900 el azul casi negro (el extremo oscuro), y blue.500 el del medio. El número no dice qué tono es —blue.500 y gray.500 son tonos completamente distintos—; dice qué tan claro u oscuro es dentro de su rampa. Por eso los steps de dos rampas distintas se corresponden en claridad: blue.100 y gray.100 son igual de claros, cada uno en su tono. Esa correspondencia es lo que te deja cambiar gray.100 por blue.100 como fondo y saber que la claridad general no cambia, solo el matiz.

¿Por qué numerar de 50 a 900 y no de 1 a 10? Es una convención heredada de la tipografía de las fuentes (los pesos: 400 normal, 700 negrita) que Material Design llevó al color y Tailwind popularizó. Lo importante no es el número exacto sino que sea ordenable y espaciado: 50, 100, 200… 900 deja hueco para intercalar (Tailwind agregó 950 después) y comunica de un vistazo la posición en la rampa. Un número bajo = claro; uno alto = oscuro.

Guarda la imagen: una paleta es un juego de rampas —una por tono—, cada una un recorrido de claridad en steps numerados 50 (claro) a 900 (oscuro); el step numera la claridad, no el tono, y por eso los steps se corresponden entre rampas.

Ejemplo trabajado: la anatomía de una rampa

Una rampa es un objeto de steps a valores hex. Aquí está la rampa blue de Mercado (los mismos valores de Tailwind, de donde salió blue.600 = #2563eb en el módulo 2). Para ver que es una rampa de claridad —que baja de forma ordenada del claro al oscuro— calculamos un proxy crudo de claridad: el promedio de los canales RGB. Ojo con esto: el promedio de canales no es la claridad que percibe el ojo humano (esa es la luminancia relativa, que construimos en la lección 6 y que pondera el verde mucho más que el azul). El promedio sirve solo aquí, para confirmar que la rampa es monótona —cada step es más oscuro que el anterior—.

// L4 — una paleta como rampa de steps 50..900. El step numera la CLARIDAD, no el tono.
const blue = {
  50:  '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
  500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a',
};

// hexToRgb: '#rrggbb' -> [r, g, b] en 0..255.
function hexToRgb(hex) {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}

// channelAverage: proxy CRUDO de claridad (promedio de canales, 0..255).
// NO es la luminancia real percibida — esa se construye en L6. Solo sirve
// aqui para ver que la rampa baja de forma monotona (50 claro -> 900 oscuro).
function channelAverage(hex) {
  const [r, g, b] = hexToRgb(hex);
  return Math.round((r + g + b) / 3);
}

console.log('=== la rampa blue de Mercado (steps 50..900) ===\n');
console.log('step'.padEnd(10) + 'hex'.padEnd(10) + 'claridad (proxy 0..255)');
console.log('-'.repeat(42));
for (const step of Object.keys(blue)) {
  const avg = channelAverage(blue[step]);
  const bar = '#'.repeat(Math.round(avg / 12));
  console.log(('blue.' + step).padEnd(10) + blue[step].padEnd(10) + String(avg).padEnd(5) + bar);
}

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== la rampa blue de Mercado (steps 50..900) ===

step      hex       claridad (proxy 0..255)
------------------------------------------
blue.50   #eff6ff   247  #####################
blue.100  #dbeafe   236  ####################
blue.200  #bfdbfe   221  ##################
blue.300  #93c5fd   199  #################
blue.400  #60a5fa   170  ##############
blue.500  #3b82f6   145  ############
blue.600  #2563eb   124  ##########
blue.700  #1d4ed8   108  #########
blue.800  #1e40af   90   ########
blue.900  #1e3a8a   75   ######

Lee la barra como la tira de lápices, del claro al oscuro. blue.50 es casi blanco (proxy 247, barra larga); blue.900 es azul profundo (proxy 75, barra corta). Y —esto es lo que importa— el proxy baja de forma monótona: cada step es más oscuro que el anterior, sin saltos ni retrocesos. Eso es lo que hace que la rampa sea usable: puedes elegir "un poco más oscuro" y sabes que subes de número. blue.600 = #2563eb es tu color.primary del módulo 2; ahora lo ves en su lugar en la rampa, el sexto de diez peldaños, ni claro ni oscuro, el azul "pleno" de marca.

Ahora, la advertencia que carga toda la lección: el promedio de canales miente sobre la claridad percibida. Fíjate que es solo un proxy para ver la monotonía. Un amarillo puro (#ffff00) y un azul puro (#0000ff) pueden tener el mismo promedio de canales, pero el ojo ve el amarillo mucho más claro que el azul —porque el ojo es muchísimo más sensible al verde y al rojo que al azul—. Por eso la claridad real, la que decide si un texto se lee sobre un fondo, no es el promedio: es la luminancia relativa, que pondera 0.2126·R + 0.7152·G + 0.0722·B (el verde pesa 7 veces más que el azul). Esa es la fórmula de la lección 6, y es la que el contraste usa. Aquí quédate con la forma de la rampa; la medida honesta de claridad viene después.

Una pregunta para conectar con lo que sigue: si blue.50 es casi blanco y blue.900 casi negro, ¿qué par de steps de una rampa esperarías que tenga más contraste entre sí —blue.400 sobre blue.500 (vecinos), o blue.100 sobre blue.800 (extremos)? (Los extremos: cuanto más separados están dos steps en la rampa, más distancia de claridad hay entre ellos, y el contraste es distancia de claridad. Dos steps vecinos casi no contrastan —por eso texto blue.400 sobre fondo blue.500 sería ilegible—. Ese es el puente a las lecciones 5–7.)

Profundización: cómo se genera una rampa y qué steps eligen los tokens

¿De dónde salen los diez valores hex de una rampa? No se eligen uno por uno a ojo. Se generan interpolando en un espacio de color: fijas el tono, defines la claridad del step 50 (muy alta) y la del 900 (muy baja), y calculas los intermedios. Las herramientas serias (como la de Tailwind, o Leonardo de Adobe) interpolan en espacios perceptualmente uniformes como OKLCH o LCH —no en RGB crudo— para que los saltos de claridad se perciban parejos, igual que la escala tipográfica usa un ratio para que los saltos de tamaño se perciban parejos. Generar una rampa perceptualmente uniforme es un tema propio (y depende de matemáticas de espacios de color que exceden este módulo); lo que necesitas saber es que una buena rampa no es una interpolación lineal en RGB —esa daría steps que se perciben desparejos, amontonados en el medio—.

Lo que sí es tu decisión de diseño es cuántas rampas tener y qué steps usar para cada rol. Una paleta típica de sistema tiene:

  • Una rampa neutral (grises), la más usada: fondos, texto, bordes. Mercado la llama gray.
  • Una o dos rampas de marca (el color primario y quizás un secundario). Mercado tiene blue.
  • Rampas semánticas de estado: verde para éxito, rojo para error/peligro, ámbar para advertencia. (Mercado las agregaría al crecer; en el proyecto nos concentramos en blue y gray.)

Y luego mapeas los roles semánticos (los tokens del módulo 2) a steps concretos:

Rol semantico (M2)   ->  step de la rampa   ->  valor
color.primary            blue.600               #2563eb
color.surface            white / gray.50        #ffffff / #f9fafb
color.text               gray.900               #111827
color.muted              gray.100 o gray.500     (segun sea fondo o texto)

Fíjate en la elegancia: el token color.primary no contiene un hex; apunta a un step de una rampa. Cambiar el azul de marca es reapuntar color.primary de blue.600 a, digamos, blue.700 —y como la rampa entera es coherente, el nuevo azul encaja con todo lo demás—. La rampa es la materia prima; los tokens eligen de ella. Esta es la conexión con el módulo 2: allá construiste los tokens que nombran roles; aquí construyes las rampas de las que esos roles toman su valor.

Anatomia de una paleta de sistema:

  gray   50 100 200 300 400 500 600 700 800 900   <- neutral (fondos, texto, bordes)
  blue   50 100 200 300 400 500 600 700 800 900   <- marca (primary)
  green  50 100 200 300 400 500 600 700 800 900   <- exito
  red    50 100 200 300 400 500 600 700 800 900   <- peligro

  Cada fila: un tono, 10 steps de claridad. Cada columna: misma claridad, distinto tono.
  Los tokens semanticos eligen una celda: color.primary = blue.600.

Errores comunes

Creer que el step numera el tono. Qué pasa: alguien piensa que blue.500 y gray.500 "son el mismo color" o que el 500 significa algo del azul. Por qué pasa: el número parece un identificador del color, no de su claridad. Cómo detectarlo: intentas usar el step para elegir tono ("quiero el 600 de cualquier cosa") en vez de claridad. Cómo corregirlo: el step numera la posición en la rampa —la claridad—, no el tono. blue.500 y gray.500 están igual de claros/oscuros pero son tonos distintos. El tono lo da el nombre de la rampa (blue, gray); el step da la claridad dentro de ella. Pensar el step como claridad es lo que te deja razonar sobre contraste después: un texto necesita mucha distancia de step respecto a su fondo, sin importar los tonos.

Elegir colores sueltos en vez de una rampa. Qué pasa: se necesita un azul y se teclea #3b82f6, y más tarde otro azul #2c6ecb, y otro #1e5fbf —todos "azules de la marca" pero sin relación—. Por qué pasa: es más rápido pegar un hex que definir una rampa completa. Cómo detectarlo: tu CSS tiene varios azules parecidos pero distintos, ninguno parte de una serie. Cómo corregirlo: define una rampa blue (diez steps coherentes) y toma de ella. Tres azules sueltos son el drift del módulo 1 en el eje del color: se ven casi iguales, nadie sabe cuál es "el" azul, y cambiarlos todos a la vez es imposible. Una rampa te da azules que pertenecen juntos y un solo lugar para cambiarlos.

Interpolar la rampa linealmente en RGB. Qué pasa: se genera la rampa promediando en RGB entre el step claro y el oscuro, y los steps intermedios salen amontonados o con tonos raros (un gris que "se vuelve morado" en el medio). Por qué pasa: la interpolación lineal en RGB es la más obvia, pero RGB no es perceptualmente uniforme. Cómo detectarlo: los saltos de tu rampa se sienten desparejos —mucho cambio entre unos steps, casi nada entre otros— o aparecen matices que no querías. Cómo corregirlo: usa una herramienta que interpole en un espacio perceptual (OKLCH/LCH) —Tailwind, Leonardo, o un generador de paletas—, o copia una rampa ya hecha (las de Tailwind están afinadas). Generar rampas perceptualmente parejas es un oficio propio; para este módulo, lo correcto es no inventarlas a mano en RGB.

Ejercicios

Ejercicio 1 — Step y claridad. Sin correr nada, responde:

  • (a) ¿Cuál es más claro, gray.100 o gray.700?
  • (b) blue.500 y red.500, ¿comparten tono o claridad?
  • (c) Para un texto oscuro sobre un fondo claro, ¿qué steps elegirías (aproximadamente) para el texto y para el fondo?
Ver solución
  • (a) gray.100. Un número más bajo es más claro (50 es el extremo claro, 900 el oscuro). gray.100 está cerca del blanco; gray.700 es un gris oscuro.
  • (b) Comparten claridad, no tono. El 500 dice "medio de la rampa" en ambos, así que están igual de claros/oscuros —pero uno es azul y el otro rojo—. El step nunca dice el tono; eso lo da el nombre de la rampa.
  • (c) Texto oscuro: un step alto, como gray.900 (casi negro). Fondo claro: un step muy bajo, como gray.50 o white. La clave es la distancia: mucho step de separación = mucha diferencia de claridad = buen contraste. Texto gray.900 sobre fondo gray.50 tiene casi toda la rampa de distancia. (Cuánto contraste exactamente, y si pasa AA, se mide en las lecciones 5–7.)

Ejercicio 2 — Predice la salida. Sin correr nada, di qué pasaría con la salida del ejemplo si recorriéramos la rampa gray en vez de blue, con estos valores:

const gray = { 50: '#f9fafb', 500: '#6b7280', 900: '#111827' };

Da el proxy de claridad aproximado (promedio de canales) de cada uno y el orden esperado.

Ver solución

Los promedios de canales (redondeados):

  • gray.50 #f9fafb → (249+250+251)/3 ≈ 250 (casi blanco, barra larga).
  • gray.500 #6b7280 → (107+114+128)/3 ≈ 116 (gris medio, barra media).
  • gray.900 #111827 → (17+24+39)/3 ≈ 27 (casi negro, barra corta).

El orden es monótono, de claro (50) a oscuro (900), igual que la rampa blue —esa monotonía es lo que hace usable cualquier rampa—. Nota que los grises tienen los tres canales casi iguales entre sí (por eso son grises, sin tono dominante); el promedio los describe mejor que a los colores saturados, donde ya vimos que miente sobre la claridad percibida.

Ejercicio 3 — Diseña una rampa semántica. Mercado quiere agregar un color de "peligro" (rojo) para el botón de eliminar del carrito. Sin correr nada, responde: (a) ¿cuántos steps tendría la rampa y cómo se numerarían? (b) ¿qué step usarías para el fondo del botón de peligro y cuál para su texto, y por qué? (c) ¿por qué conviene una rampa entera y no solo el rojo que usará el botón?

Ver solución
  • (a) Diez steps, numerados igual que las demás: red.50 (casi blanco) a red.900 (casi negro), pasando por red.500 (rojo pleno). La misma estructura que blue y gray —consistencia entre rampas—.
  • (b) Fondo del botón: un step medio-alto como red.600 (rojo saturado, "peligro" evidente). Texto sobre él: blanco o un step muy claro (red.50), para que contraste con el fondo oscuro. La lógica es de contraste: texto claro sobre fondo oscuro = mucha distancia de claridad. (El ratio exacto lo medirías con contrastRatio en la lección 6, igual que harás con el botón azul.)
  • (c) Porque el botón no es el único lugar donde aparecerá el peligro: habrá un texto de error (red.700 sobre blanco), un fondo de alerta sutil (red.50), un borde (red.300). Una rampa entera te da todos esos rojos coherentes entre sí y un solo lugar para ajustarlos. Un solo rojo suelto te obligaría a inventar los demás cuando los necesites —y a inventarlos sin garantía de que combinen—.

Resumen y siguiente paso

En esta lección construiste la anatomía de una paleta de color: un juego de rampas —una por tono— donde cada rampa recorre la claridad en steps numerados 50 (claro) a 900 (oscuro). Con la tira de lápices del artista viste por qué el color escala distinto a las otras dos escalas: tiene dos ejes (tono y claridad), y el step numera la claridad, no el tono —por eso blue.500 y gray.500 comparten claridad y no tono—. Y lo comprobaste ejecutando: la rampa blue bajó de forma monótona del casi-blanco (blue.50) al azul profundo (blue.900), con blue.600 = #2563eb, tu color.primary, en su lugar. También viste la advertencia que carga el resto del módulo: el promedio de canales es un proxy que miente sobre la claridad percibida; la medida honesta es la luminancia relativa.

Antes de avanzar deberías poder: explicar qué numera un step (la claridad, no el tono); decir por qué los steps se corresponden entre rampas; y anticipar que el contraste entre dos colores es la distancia entre sus claridades —así que dos steps vecinos casi no contrastan y dos extremos sí—.

Esa última intuición es la puerta de la lección 5: el contraste y WCAG. Si el contraste es distancia de claridad, ¿cuánta distancia hace falta para que un texto se lea? La respuesta no es una opinión: es un número con un umbral fijo. La lección 5 define qué es el ratio de contraste, sus niveles AA y AAA, y por qué un sistema de diseño debe garantizarlo para ser accesible. Es donde el color deja de ser estética y pasa a ser medición.

Recursos

  • Tailwind CSS, "Colors" — tailwindcss.com/docs/colors. Las rampas por steps (50–950) que este módulo toma como modelo; explora blue y gray, las de Mercado. En inglés.
  • Material Design, "The color system" — m2.material.io/design/color/the-color-system.html. El sistema que popularizó numerar el color por claridad; el origen de la convención 50–900. En inglés.
  • Adobe Leonardo — leonardocolor.io. Un generador de rampas que interpola en espacios perceptuales y muestra el contraste de cada step; el laboratorio de esta lección y puente a la 6. En inglés.
  • MDN, "Understanding relative luminance" — vía developer.mozilla.org, "WCAG color contrast". La medida honesta de claridad que reemplaza al proxy de esta lección; léela antes de la 6. En inglés.