wandres.dev
GRADIENTES · Interpolación, ángulos y cónicos

Paradas, posiciones y la sugerencia de interpolación

Cómo rellena el navegador las posiciones que no escribes, qué pasa cuando una parada va por detrás de la anterior, y el valor suelto entre dos colores que casi nadie usa.

⏱ 17 min

La lista de paradas de un degradado tiene reglas de relleno automático que casi nunca se leen y que explican por qué un degradado de cuatro colores no siempre reparte el espacio como esperabas. Entre esas reglas hay dos joyas: una que convierte una posición mal puesta en una transición dura, y otra que te deja mover el punto medio de una transición sin añadir ningún color.

🎯 Al terminar esta lección sabrás
  • Predecir las posiciones que el navegador asigna a las paradas sin posición.
  • Provocar una transición dura aprovechando el recorte de posiciones decrecientes.
  • Usar la sugerencia de interpolación para desplazar el punto medio.
  • Escribir fundidos a transparente que no ensucien el color.

Las posiciones y cómo se rellenan

Cada parada puede llevar una posición en longitud o en porcentaje. Las que no la llevan se calculan con tres reglas que se aplican en orden:

La primera parada sin posición vale 0% y la última vale 100%. Por eso linear-gradient(red, blue) va de un extremo al otro.

Las paradas intermedias sin posición se reparten uniformemente entre las posiciones conocidas que las rodean. En linear-gradient(red, green, blue, black), las cuatro quedan en 0%, 33.3%, 66.6% y 100%.

Y aquí está lo interesante: el reparto se hace por tramos entre paradas con posición explícita, no sobre el degradado entero.

/* red 0%, green 10%, blue 55%, black 100% */
.a { background: linear-gradient(red, green 10%, blue, black); }

blue no está en el 66.6%: está en el punto medio entre green al 10% y black al 100%, es decir en el 55%. Poner una sola posición explícita reordena todo lo que viene después, y ese es el motivo de que añadir una parada a un degradado que funcionaba lo descoloque entero.

Posiciones decrecientes y transiciones duras

La especificación obliga a que la lista de posiciones sea no decreciente. Si escribes una posición menor que la anterior, no es un error: se recorta hacia arriba hasta igualar a la anterior.

/* La segunda parada se recorta de 20% a 60%. */
.a { background: linear-gradient(red 60%, blue 20%); }

El resultado es que las dos paradas comparten posición, la transición ocurre en una distancia de cero, y ves una línea dura. Eso es exactamente lo que se aprovecha para dibujar bandas, y es la razón de que exista la sintaxis de doble posición:

/* Dos paradas en una: rojo de 0 a 50%, azul de 50% a 100%. */
.a { background: linear-gradient(red 0 50%, blue 50% 100%); }

red 0 50% significa una parada roja en 0 y otra parada roja en 50%. Es azúcar sintáctico para escribir red 0, red 50%, y hace que las listas de bandas se lean mucho mejor.

Un detalle que ahorra depuraciones: el recorte usa la posición ya resuelta, no la escrita. Si mezclas longitudes y porcentajes en una caja cuyo tamaño no conoces, una parada en 20px puede acabar por detrás de una en 10% en cajas grandes y por delante en cajas pequeñas, produciendo una banda dura que aparece y desaparece con el ancho.

La sugerencia de interpolación

Entre dos paradas se puede escribir un valor de posición suelto, sin color. No es una parada: es una sugerencia que indica dónde debe alcanzarse el color intermedio de la transición.

/* El punto medio de la transicion cae en el 25%, no en el 50%. */
.a { background: linear-gradient(red, 25%, blue); }

/* Con paradas posicionadas. */
.b { background: linear-gradient(red 20%, 40%, blue 80%); }

Sin sugerencia, el color intermedio entre las dos paradas cae en el punto medio geométrico y la interpolación es lineal. Con sugerencia, la interpolación deja de ser lineal: el motor usa una curva de potencia calculada para que el color medio caiga exactamente donde has dicho.

Eso tiene un uso muy concreto y muy poco explotado: controlar la velocidad de un fundido sin añadir colores. Un desvanecimiento de un contenido hacia el borde inferior de un contenedor se ve mucho mejor si el fundido empieza despacio y acelera al final, y eso son dos caracteres:

.desvanecido {
  mask-image: linear-gradient(to bottom, black 60%, 80%, transparent);
}

Comparado con la alternativa —meter tres paradas intermedias a ojo— la sugerencia es más corta, más fácil de ajustar, y produce una curva suave en lugar de una poligonal.

ℹ️
Una sugerencia por hueco, y solo entre dos paradas

No puedes poner dos sugerencias seguidas ni una sugerencia al principio o al final de la lista. Cada hueco entre dos paradas admite exactamente una. Si necesitas más control que eso, lo que necesitas son más paradas.

Fundidos a transparente

El fundido a transparente tuvo mala fama durante años porque transparent es, literalmente, rgb(0 0 0 / 0): negro con alfa cero. Interpolando ingenuamente el canal de color junto con el alfa, un fundido de rojo a transparente pasaba por rojos cada vez más oscuros y dejaba una franja gris sucia.

La especificación lo resolvió mandando que la interpolación de colores con alfa se haga en espacio premultiplicado: cada canal de color se multiplica por su alfa antes de interpolar y se divide después. Con eso, el canal de color del extremo transparente no aporta nada, porque va multiplicado por cero, y el fundido conserva el tono.

Aun así, hay dos razones para escribirlo explícito:

/* Funciona, y depende del premultiplicado. */
.a { background: linear-gradient(to bottom, #3364db, transparent); }

/* Explicito: mismo color, alfa cero. No depende de nada. */
.b { background: linear-gradient(to bottom, #3364db, #3364db00); }

/* Igual, derivado de una custom property. */
.c { background: linear-gradient(to bottom, var(--c), oklch(from var(--c) l c h / 0)); }

La primera razón es que en un espacio polar el transparent sigue aportando su ángulo de tono, que es cero, y eso puede girar el fundido hacia el rojo. La segunda es que la forma explícita se lee mejor: dice “este color desapareciendo”, que es lo que querías, en vez de “este color acercándose a un negro invisible”.

Las paradas se resuelven contra la caja de pintado, no contra el elemento

Hay un desajuste que produce degradados que se ven distintos de un elemento a otro con el mismo CSS, y la causa es que los porcentajes de las paradas no se resuelven contra el elemento sino contra el área de posicionamiento del fondo, que por defecto es la caja de relleno y que background-origin puede cambiar. Añadir un borde grueso, cambiar background-origin a border-box, o poner background-size a algo que no sea el tamaño natural del área, mueve todas las paradas a la vez. En un degradado suave eso no se nota; en una banda dura se ve como un salto de varios píxeles. El caso donde más muerde es el patrón de rayas repetidas: un repeating-linear-gradient con paradas en píxeles se alinea perfectamente en un elemento sin borde y se descuadra en cuanto alguien le pone border: 1px, porque el origen del degradado se desplaza un píxel y todas las rayas con él. La regla que evita el problema entero es fijar el origen explícitamente en cualquier degradado con geometría dura: background-origin: border-box junto con background-clip: border-box hace que las paradas se resuelvan contra un rectángulo que no depende del relleno ni del borde, y a partir de ahí el patrón es estable frente a cualquier cambio de caja. Para degradados suaves da igual y no merece la pena escribirlo; para rayas, damero, cuadrículas y cualquier cosa medida en píxeles, es la diferencia entre un patrón que aguanta y uno que hay que retocar cada vez que alguien toca el borde.

⚔️ Domina la lista de paradas
  1. Escribe linear-gradient(red, green 10%, blue, black) y verifica en el inspector dónde cae blue.
  2. Provoca una transición dura poniendo una posición menor que la anterior y comprueba que no hay error.
  3. Reescribe la misma banda con la sintaxis de doble posición.
  4. Haz un desvanecimiento con una sugerencia de interpolación y compáralo con la versión lineal.
  5. Compara un fundido a transparent con uno a la forma explícita en un espacio polar.