Degradados, bandeado y por qué el centro se ve turbio
Publicado el 21/8/2025 · 18 min de lectura · Herramientas para desarrolladores
Daniel Okonkwo — Desarrollador front-end y redactor de Tecnología en Allin
Rendimiento web · Formatos de archivo
Verificado con 6 fuentes
Un degradado CSS interpola los números guardados en los colores, y esos números están codificados en gamma. sRGB gasta sus 8 bits donde el ojo es sensible en vez de en proporción a la luz, así que promediar dos valores codificados no da el color con la cantidad media de luz. Toma rojo #ff0000 a verde #00ff00. El punto medio sRGB simple es #808000, con luminancia relativa WCAG de 0,1986. Lineariza ambos extremos, promédialos allí y vuelve a codificar: obtienes #bcbc00 con luminancia 0,4639 — exactamente la media de las luminancias de los extremos, como debe ser media luz. El punto medio por defecto queda un 57,2 % por debajo, y ese déficit es la banda oscura y turbia que se ve en el centro de todo degradado de dos colores. Un segundo problema, sin relación, es el bandeado. Ocho bits por canal dan 256 valores, así que una rampa entre dos colores oscuros cercanos puede contener sólo 28 colores distintos; estirada sobre 1920 píxeles eso es una banda de 68,6 píxeles de ancho medio, visible desde el otro lado de la habitación. El tramado lo oculta cambiando ruido espacial por resolución tonal. CSS ya permite corregir el primer problema directamente con `in oklab` o `in oklch`, y la palabra clave de dirección de tono decide por qué lado de la rueda viaja el color.
Interpolar en sRGB promedia números codificados en gamma: el punto medio de rojo a verde es #808000 cuando la respuesta a media luz es #bcbc00 — un 57,2 % menos de luz. El bandeado es otro problema aritmético: 8 bits dan 256 escalones, y un degradado oscuro puede tener sólo 28. Aquí están ambos, calculados.
La media se toma en las unidades equivocadas
Un linear-gradient en CSS hace lo obvio: para cada píxel calcula una posición t entre 0 y 1 y devuelve los colores extremos mezclados según t. La sutileza es qué mezcla. Un código hexadecimal no es una medida de luz; es una palabra de código, y sRGB asigna esas palabras deliberadamente de forma no lineal para gastar los 256 valores disponibles donde la visión humana puede distinguirlos. El valor almacenado es aproximadamente la luz elevada a 1/2,2, no la luz misma.
La función de transferencia sRGB está escrita en la norma y es fácil de implementar. Para decodificar, divide el valor de 8 bits entre 255 para obtener c y devuelve c ÷ 12,92 si c es como mucho 0,04045, y ((c + 0,055) ÷ 1,055) elevado a 2,4 en caso contrario. Para codificar, invierte: multiplica por 12,92 por debajo de 0,0031308, si no 1,055 por el valor elevado a 1/2,4, menos 0,055. Esas dos funciones bastan para pasar de palabras de código a luz y volver.
Pasa un solo número y el tamaño del problema ya se ve. La mitad de la luz — lineal 0,5 — se codifica como 0,735357, que es 188 en 8 bits, o #bcbcbc. El color que casi todos llaman gris del cincuenta por ciento, #808080, se decodifica como 0,215861. No es la mitad de nada. Es la palabra de código que está a mitad del código, cosa completamente distinta de la mitad de la luz.
De rojo a verde, medido de las dos formas
Toma #ff0000 hacia #00ff00 y calcula el punto medio dos veces. La forma ingenua promedia los valores guardados: (255 + 0) ÷ 2 = 127,5 para el rojo, igual para el verde, 0 para el azul. Eso da #808000, y pasarlo por el cálculo de luminancia WCAG da 0,1986. La forma correcta decodifica primero: el rojo pasa a lineal (1, 0, 0) y el verde a (0, 1, 0), la media es (0,5, 0,5, 0), y al recodificar salen 187,5 en ambos canales — #bcbc00, con luminancia 0,4639.
El segundo número es verificable de forma independiente: la luminancia relativa de #ff0000 es 0,2126 y la de #00ff00 es 0,7152, y su media es 0,4639 — exactamente lo que mide el punto medio lineal. Esa es la definición de mitad en luz, y el método lineal la reproduce hasta el último dígito. El punto medio sRGB entrega 0,1986, un 57,2 % menos. Falta bastante más de la mitad de la luz que debería estar en el centro del degradado.
El mismo déficit aparece dondequiera que los dos extremos estén lejos en el espacio de códigos. Negro a blanco: punto medio sRGB #808080 con luminancia 0,2140, punto medio lineal #bcbcbc con 0,5000, el idéntico déficit del 57,2 %. Blanco a azul: #8080ff con 0,2708 frente a #bcbcff con 0,5361, un déficit del 49,5 %. No es una rareza de colores concretos; es la forma de la función de transferencia, y se aplica a todo degradado por defecto en la web.
De azul a amarillo pasa por gris — otro fallo con otro remedio
Es tentador achacar también al problema de gamma el centro gris de un degradado azul-amarillo, y resulta que es falso. Calcula ambos puntos medios para #0000ff hacia #ffff00. El punto medio sRGB es #808080 con luminancia 0,2140. El punto medio en luz lineal es #bcbcbc con luminancia 0,5000. La versión lineal es mucho más clara, tal como predice la sección anterior — y sigue siendo un gris perfectamente neutro. Medidos en OKLCH, ambos tienen un croma de 0,000. Trabajar en luz lineal arregló la oscuridad y no hizo absolutamente nada contra el gris.
La razón es que azul y amarillo son casi opuestos en el cubo RGB, así que promediar sus canales cancela el color sean cuales sean las unidades. Toda interpolación rectangular — sRGB, sRGB lineal, incluso OKLab, que es rectangular pese a ser perceptual — recorre una recta que pasa por el centro del espacio de color, y el centro del espacio de color es neutro. Sólo una interpolación polar, que transporta el croma y rota el tono en lugar de promediar canales opuestos, rodea por fuera.
Una demostración más limpia usa un par que se queda dentro del gamut sRGB: #007bff hacia #ff6b00, un azul y un naranja cuyos tonos OKLCH son 257,2° y 45,1°. En sRGB simple el punto medio es #807380, un gris malva turbio con croma OKLCH de 0,024 — el color ha muerto. En sRGB lineal es #bc73bc, croma 0,131. En OKLab es #a285a5, croma 0,058. En OKLCH por el arco de tono corto es #d454c9, croma 0,208: un magenta vivo, y el croma es ahora aproximadamente la media de los extremos en lugar de una fracción. Los mismos dos topes, cinco centros distintos.
Bandeado: 256 valores estirados sobre 2560 píxeles
El bandeado no tiene nada que ver con el espacio de interpolación. Es un problema de conteo. Un canal de 8 bits tiene 256 valores posibles, así que un degradado nunca puede contener más colores distintos de los que abarcan sus extremos. Si una rampa se dibuja sobre más píxeles de los que tiene valores distintos, algunos valores tienen que repetirse, y cada tramo repetido es una franja plana.
Rasterizar las rampas píxel a píxel y contar da las cifras reales. Un degradado completo de negro a blanco contiene 256 colores distintos sin importar lo ancho que se dibuje. Sobre 320 píxeles la banda media es de 1,3 píxeles y el tramo plano más largo de 2: nadie los verá. Sobre 1920 píxeles la media sube a 7,5 píxeles y el tramo más largo a 8. Sobre 2560 píxeles es 10,0 y 11 — una raya visible, en el rango tonal más amplio posible.
Los degradados reales son mucho peores, porque los diseñadores rara vez cubren todo el rango. Un hero oscuro de moda que va de #1a1a2e a #16213e contiene sólo 28 colores distintos. Sobre 1920 píxeles eso da una banda media de 68,6 píxeles, con el tramo plano más largo llegando a 120 píxeles. Sobre 2560 píxeles la media es de 91,4 píxeles. Una superficie sutil de blanco sobre blanco, de #ffffff a #f5f5f5, contiene 11 colores: a 1920 píxeles la banda media mide 174,5 píxeles. No son artefactos que un visitante quizá note; son rayas más anchas que un botón.
De aquí sale una regla utilizable. Si tomas 3 píxeles como la anchura a partir de la cual una banda plana se lee como un borde y no como tono continuo, el bandeado se hace visible en cuanto la anchura dibujada supera tres veces el número de niveles distintos. Una rampa completa de 256 niveles aguanta hasta 768 píxeles. Una de 52 niveles aguanta hasta 156. El hero de 28 niveles aguanta hasta 84 píxeles, y la superficie de 11 niveles hasta 33. Casi todos los degradados decorativos de una página moderna están dibujados muy por encima de su límite.
El tramado, y qué darían más bits
El tramado es el remedio estándar y funciona cambiando un tipo de error por otro. En vez de redondear cada píxel al valor disponible más cercano, se añade algo de ruido antes de redondear, de modo que los píxeles cerca de una frontera se reparten entre los dos valores vecinos en proporción a lo cerca que estén. La media sobre cualquier zona pequeña resulta entonces correcta aunque ningún píxel lo sea, y el ojo integra la zona en vez del píxel. La raya visible se sustituye por un grano tenue.
En la web rara vez controlas el rasterizador, así que el tramado suele aplicarse a mano: superpón sobre el degradado una textura de ruido en mosaico y de contraste muy bajo, con una opacidad del uno o dos por ciento, o genera el degradado como SVG con una capa feTurbulence. Ambos enfoques rompen los tramos planos. El truco está en mantener el ruido por debajo del umbral en que se lee como textura: si ves el grano, te has pasado y has cambiado una raya por una superficie sucia.
Más bits eliminarían la necesidad por completo. Con 10 bits por canal hay 1024 niveles, y la misma rampa de rango completo sobre 1920 píxeles da una banda media de 1,88 píxeles — por debajo del umbral visible, así que sin tramado. Las pantallas de gamut amplio y las cadenas HDR ofrecen cada vez más esa profundidad, y los colores CSS escritos con oklch() o con la función color() saben expresarla. La restricción es toda la cadena: un valor de 10 bits compuesto en un búfer de 8 bits vuelve a ser un valor de 8 bits.
Las palabras clave CSS: in oklch, in lab y la dirección de tono
CSS Color 4 permite nombrar el espacio de interpolación directamente dentro de la función de degradado: linear-gradient(in oklch, #007bff, #ff6b00). Los espacios aceptados incluyen srgb, srgb-linear, lab, oklab, lch, oklch, hsl, hwb y xyz. Los rectangulares — srgb, srgb-linear, lab, oklab, xyz — interpolan las coordenadas componente a componente. Los polares — hsl, hwb, lch, oklch — interpolan un ángulo de tono, y ahí es donde viven las sorpresas.
Para un espacio polar puedes añadir un método de interpolación de tono: shorter, longer, increasing o decreasing, cada uno seguido de la palabra hue. Shorter toma el arco de como mucho 180°, que es el valor por defecto. Longer toma el arco complementario, dando la vuelta larga. Increasing obliga al ángulo a crecer, decreasing a decrecer. Ejemplo resuelto: al interpolar del tono 20 al tono 320, shorter da un arco de −60° y el camino 20, 5, 350, 335, 320, mientras que longer da un arco de +300° y el camino 20, 95, 170, 245, 320 — por el verde y el azul, un degradado completamente distinto a partir de los mismos dos topes.
Hay una segunda cosa que vigilar en los espacios polares. OKLCH puede describir colores fuera del gamut sRGB, y el camino polar se abomba hacia afuera: el punto medio OKLCH del azul #0000ff y el amarillo #ffff00 se calcula en coordenadas que dan un triplete RGB fuera de rango y hay que someterlo a gamut mapping antes de mostrarlo. Los navegadores se encargan, pero el resultado llevará menos croma del que pedía la aritmética, así que un degradado brillante en un selector de color puede llegar bastante más apagado a la pantalla. El par dentro del gamut usado arriba evita el problema por completo, y ésa es la lección práctica: comprueba el centro, no sólo los extremos.
Desvanecer a transparente y el alfa premultiplicado
El último bug clásico de degradado es la neblina gris al final de un desvanecimiento a transparente. Ocurre cuando la interpolación trata color y alfa como cuatro números independientes. Desvanece blanco hacia `transparent`: como la palabra clave CSS `transparent` vale rgb(0 0 0 / 0) — negro transparente — el canal de color es arrastrado del blanco hacia el negro al mismo tiempo que cae el alfa. En el punto medio el color es gris al 50 % y el alfa vale 0,5, así que sobre una página blanca el píxel se dibuja como #bfbfbf: una banda sucia visible donde debería haber un desvanecimiento limpio.
El alfa premultiplicado elimina el artefacto. Antes de interpolar, multiplica cada canal de color por su propio alfa; interpola juntos los canales premultiplicados y el alfa; y luego vuelve a dividir al final. Un tope completamente transparente no aporta nada al color, porque sus canales premultiplicados son todos cero, así que no puede arrastrar el color visible a ninguna parte. Ejecutar el mismo desvanecimiento de blanco a negro transparente en premultiplicado da #ffffff en el punto medio sobre fondo blanco en vez de #bfbfbf, y #808080 sobre negro en vez de #404040.
Los degradados CSS están especificados para interpolar en espacio premultiplicado, así que los navegadores modernos lo hacen bien. El arreglo manual, que conviene conocer porque sobrevive en código canvas, en SVG, en los editores de imagen y en motores antiguos, consiste en nombrar el tope transparente explícitamente en el color del que partes: usa rgb(255 255 255 / 0) en vez de la palabra clave `transparent`. Calcula ambos puntos medios y la equivalencia es exacta — desvanecer blanco a blanco transparente da #ffffff sobre blanco y #808080 sobre negro con cualquiera de los dos métodos, premultiplicado o no.
| Espacio de interpolación | CSS | Punto medio | Croma OKLCH | Resultado |
|---|---|---|---|---|
| sRGB (por defecto) | in srgb | #807380 | 0,024 | Oscuro y casi incoloro |
| sRGB en luz lineal | in srgb-linear | #bc73bc | 0,131 | Brillo correcto, tono apagado |
| OKLab (rectangular) | in oklab | #a285a5 | 0,058 | Claridad uniforme, aún deslavado |
| OKLCH, arco corto | in oklch shorter hue | #d454c9 | 0,208 | Magenta vivo, croma preservado |
| OKLCH, arco largo | in oklch longer hue | #00b145 | 0,208 | Verde, fuera de gamut y recortado |
Preguntas frecuentes
- ¿Por qué mi degradado de dos colores tiene una banda oscura en el centro?
- Porque el navegador promedió números codificados en gamma en vez de luz. sRGB guarda un valor cercano a la luz elevada a 1/2,2, así que la media aritmética de dos valores guardados corresponde a mucha menos luz que la media de las dos luces. Rojo #ff0000 a verde #00ff00 lo concreta: el punto medio sRGB simple es #808000 con luminancia relativa 0,1986, mientras que el verdadero punto medio a media luz es #bcbc00 con 0,4639 — exactamente la media de las luminancias de los extremos, 0,2126 y 0,7152. El comportamiento por defecto queda un 57,2 % por debajo de la luz que debería llevar, y ese déficit es la banda oscura. El arreglo de una línea en CSS es nombrar un espacio de interpolación mejor: linear-gradient(in oklab, red, lime) o linear-gradient(in srgb-linear, red, lime). Ambos elevan el centro. Si además quieres que el centro siga siendo colorido en vez de volverse neutro, necesitas un espacio polar — in oklch — porque ése es un problema aparte, descrito en la respuesta siguiente.
- ¿Por qué de azul a amarillo pasa por gris incluso en un espacio mejor?
- Porque el gris y la oscuridad son fallos distintos. Azul y amarillo están casi opuestos, así que promediar sus canales cancela el color sean cuales sean las unidades. Los números lo confirman: el punto medio sRGB de #0000ff y #ffff00 es #808080 con luminancia 0,2140, y el punto medio en luz lineal es #bcbcbc con luminancia 0,5000. La versión lineal es mucho más clara, como se esperaba, y ambas tienen croma OKLCH de 0,000 — perfectamente neutro. Incluso OKLab, pese a ser perceptual, da #6cabc7 con un croma de sólo 0,077, porque OKLab es rectangular y una recta entre dos colores opuestos pasa por el eje neutro. Sólo una interpolación polar lo evita, porque rota el tono y transporta el croma en vez de promediar canales opuestos. Usa linear-gradient(in oklch, blue, yellow). Ten en cuenta que para este par concreto el punto medio polar cae fuera del gamut sRGB y se le aplicará gamut mapping, así que obtendrás menos croma del prometido. Si quieres un centro vivo con seguridad, añade un tope intermedio explícito en el tono que realmente quieres.
- ¿Cuánto puede medir un degradado antes de empezar a bandear?
- Depende enteramente de lo separados que estén los extremos, no del tamaño del elemento. Cuenta los colores de 8 bits distintos que puede contener la rampa y multiplica por unos tres: tomando 3 píxeles como la anchura a partir de la cual una banda plana se lee como un borde, ese producto es la anchura a la que aparece el bandeado. Una rampa completa de negro a blanco contiene 256 colores y aguanta hasta unos 768 píxeles. Una rampa de #0f172a a un pizarra cercano contiene 52 colores y aguanta hasta 156. Un hero oscuro de moda de #1a1a2e a #16213e contiene sólo 28 colores y bandea más allá de 84 píxeles — es decir, bandea en todos los diseños en que aparecerá. Una superficie casi blanca de #ffffff a #f5f5f5 contiene 11 colores y cede a 33 píxeles; dibujada sobre 1920, su banda media mide 174,5 píxeles. Tres movimientos prácticos: ampliar la distancia tonal entre los topes para disponer de más niveles, añadir un ruido tenue al uno o dos por ciento de opacidad para tramar las fronteras, o trazar el degradado en diagonal en vez de en línea recta, lo que alarga la rampa respecto al elemento y desalinea los bordes de banda del diseño.
- ¿Qué hace realmente `longer hue` y cuándo lo querría?
- Obliga al tono a dar la vuelta larga al círculo en vez de tomar el arco corto. En una interpolación polar el navegador tiene dos caminos candidatos entre dos ángulos de tono y por defecto elige el de como mucho 180°. Al interpolar del tono 20 al tono 320, `shorter hue` da un arco de −60° y pasa por 5, 350 y 335 — un recorrido corto por los rojos. `longer hue` da un arco de +300° y pasa por 95, 170 y 245 — verdes, cianes y azules, es decir todo el espectro. Los mismos dos topes, degradado completamente distinto. Vale la pena echar mano de la palabra clave cuando quieres un barrido de arcoíris a partir de dos extremos sin escribir topes intermedios, algo habitual en indicadores de progreso, rampas de visualización de datos y bordes decorativos. Hay dos palabras clave más: `increasing hue` obliga al ángulo a crecer y `decreasing hue` a decrecer, que es como controlas la dirección y no sólo la longitud. Ojo al caso límite en que los dos tonos están exactamente a 180°: shorter y longer describen entonces la misma longitud de arco y hay que usar increasing o decreasing para decir por dónde.
- ¿Por qué mi desvanecimiento a transparente se ve sucio en el borde?
- Casi siempre porque el tope transparente es negro transparente. La palabra clave CSS `transparent` se expande a rgb(0 0 0 / 0), así que un degradado escrito de blanco `to transparent` arrastra el canal de color del blanco hacia el negro mientras cae el alfa. Si color y alfa se interpolan por separado, el punto medio es gris al 50 % con alfa 0,5, que compuesto sobre una página blanca da #bfbfbf — una neblina gris visible. Interpolar en espacio premultiplicado lo arregla: multiplica cada color por su alfa antes de mezclar, mezcla y vuelve a dividir; un tope totalmente transparente ya no aporta color porque sus canales premultiplicados son cero. El mismo punto medio se compone entonces como #ffffff sobre blanco y #808080 sobre negro. Los degradados CSS están especificados en premultiplicado, así que los navegadores actuales aciertan aquí; el bug sobrevive en código canvas, en SVG, en editores de imagen y en motores antiguos. El arreglo portátil es escribir el tope en el color del que partes — rgb(255 255 255 / 0) en vez de `transparent` — que da resultados idénticos con cualquiera de los dos métodos y no cuesta nada.
Artículos que podrían interesarte
Todas las guías →Herramientas relacionadas
Fuentes
- W3C — CSS Color Module Level 4 — interpolation, colour spaces, hue interpolation methods and premultiplied alpha
- W3C — CSS Images Module Level 4 — gradient interpolation and colour stop resolution
- IEC — IEC 61966-2-1:1999 — Default RGB colour space (sRGB), the piecewise transfer function
- Charles Poynton — Gamma FAQ — why encoding is nonlinear and what breaks when you average encoded values
- Björn Ottosson — A perceptual color space for image processing (OKLab), 2020
- W3C — Web Content Accessibility Guidelines (WCAG) 2.2 — the relative luminance definition used for the midpoint measurements
¿Has detectado un error en este artículo?