wandres.dev
GRADIENTES Y PATRONES · Rellenos complejos

linearGradient y las paradas de color

La geometría del vector de gradiente, cómo se ordenan y se corrigen las paradas, la herencia entre gradientes con href, y por qué transparent no siempre hace lo que esperas.

⏱ 16 min

Un gradiente lineal es un vector y una lista de paradas: todo lo demás es consecuencia. El vector dice en qué dirección varía el color y dónde empieza y acaba la variación; las paradas dicen qué color hay en cada punto del recorrido. Con esas dos ideas se explica desde el degradado más simple hasta la banda dura que simula una franja de color, y también el bug del degradado a transparente que sale gris por el medio.

🎯 Al terminar esta lección sabrás
  • Situar el vector de gradiente con x1, y1, x2 e y2 y predecir la dirección resultante.
  • Ordenar paradas y anticipar la corrección automática cuando el offset decrece.
  • Reutilizar paradas entre gradientes con href.
  • Evitar la zona gris de un degradado hacia transparente.

El vector

linearGradient define un vector con cuatro coordenadas. El color varía a lo largo de la proyección de cada punto sobre ese vector: perpendicular al vector, el color es constante.

<svg viewBox="0 0 200 100" width="400">
  <defs>
    <linearGradient id="horizontal">
      <stop offset="0"   stop-color="#89b4fa" />
      <stop offset="1"   stop-color="#f38ba8" />
    </linearGradient>
    <linearGradient id="diagonal" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0"   stop-color="#a6e3a1" />
      <stop offset="1"   stop-color="#cba6f7" />
    </linearGradient>
  </defs>
  <rect x="5"   y="5" width="90" height="90" fill="url(#horizontal)" />
  <rect x="105" y="5" width="90" height="90" fill="url(#diagonal)" />
</svg>

Los valores por omisión son x1="0%", y1="0%", x2="100%", y2="0%": un vector horizontal de izquierda a derecha. Eso explica por qué un linearGradient sin coordenadas siempre sale horizontal.

Para un gradiente vertical, el vector es x1="0" y1="0" x2="0" y2="1". Para uno a 45 grados, x1="0" y1="0" x2="1" y2="1" si el elemento es cuadrado; si no lo es, el ángulo aparente se deforma, y esa deformación es una de las dos consecuencias del sistema de coordenadas que se explica en gradientUnits y los dos sistemas.

gradientTransform aplica una transformación al espacio del gradiente, y es la forma limpia de rotar sin recalcular coordenadas:

<linearGradient id="g45" gradientTransform="rotate(45 0.5 0.5)">
  <stop offset="0" stop-color="#94e2d5" />
  <stop offset="1" stop-color="#fab387" />
</linearGradient>

La rotación se hace alrededor del centro del espacio unitario, de ahí el 0.5 0.5.

Las paradas

Cada stop tiene offset (0 a 1, o un porcentaje), stop-color y stop-opacity. Los tres son propiedades de presentación, así que se pueden escribir desde CSS, y eso permite tematizar un gradiente sin tocar el marcado.

Tres reglas de la especificación que conviene saber de memoria:

Uno: los offset se recortan al rango 0 a 1. Un offset="1.5" vale 1.

Dos: si una parada tiene un offset menor que la anterior, se corrige al valor de la anterior. Es decir, la lista se fuerza a ser no decreciente. Esto no es un error que puedas cometer sin consecuencias, es un mecanismo que se usa a propósito.

Tres: dos paradas con el mismo offset producen una transición dura. Sale de la regla anterior y es la forma de hacer bandas de color sólidas:

<linearGradient id="bandas">
  <stop offset="0"    stop-color="#a6e3a1" />
  <stop offset="0.33" stop-color="#a6e3a1" />
  <stop offset="0.33" stop-color="#f9e2af" />
  <stop offset="0.66" stop-color="#f9e2af" />
  <stop offset="0.66" stop-color="#f38ba8" />
  <stop offset="1"    stop-color="#f38ba8" />
</linearGradient>

Tres franjas nítidas con un solo elemento de relleno. Es la técnica para una barra de umbrales en un gráfico, o para un indicador de progreso por tramos.

Sin ninguna parada, el elemento se pinta como si tuviera fill="none". Con una sola parada, se pinta de ese color sólido. Son casos límite útiles: un gradiente generado por código puede colapsar a color plano sin ramificar.

Herencia entre gradientes con href

Un gradiente puede heredar las paradas de otro. Es la manera de tener una paleta única y varias orientaciones sin duplicar la lista:

<defs>
  <linearGradient id="paleta">
    <stop offset="0"   stop-color="#89b4fa" />
    <stop offset="0.5" stop-color="#cba6f7" />
    <stop offset="1"   stop-color="#f38ba8" />
  </linearGradient>

  <linearGradient id="vertical" href="#paleta" x1="0" y1="0" x2="0" y2="1" />
  <linearGradient id="diagonal" href="#paleta" x1="0" y1="0" x2="1" y2="1" />
</defs>

La herencia alcanza a las paradas y también a los atributos que el gradiente hijo no declare: gradientUnits, spreadMethod, gradientTransform. Los que sí declara ganan.

En SVG 1.1 esto se escribía xlink:href y hacía falta declarar el espacio de nombres. SVG 2 lo simplificó a href y los motores actuales lo aceptan. Si mantienes un fichero antiguo verás la forma con prefijo, y funciona igual; en fichero nuevo no hay razón para usarla.

Un gradiente radial puede heredar las paradas de uno lineal y viceversa: lo que se hereda es la lista de colores, no la geometría.

El bug de la zona gris

Un degradado de un color a transparente parece trivial:

<!-- Mal: pasa por gris -->
<linearGradient id="mal">
  <stop offset="0" stop-color="#89b4fa" />
  <stop offset="1" stop-color="transparent" />
</linearGradient>

Y produce un degradado que en la zona intermedia se ensucia hacia el gris. La razón: transparent es negro con alfa cero. La interpolación se hace componente a componente, así que en el punto medio el color es el promedio entre azul y negro, con alfa 0,5. El resultado es un azul apagado, casi gris.

La solución es declarar la parada final con el mismo color y opacidad cero:

<!-- Bien: el matiz se mantiene -->
<linearGradient id="bien">
  <stop offset="0" stop-color="#89b4fa" stop-opacity="1" />
  <stop offset="1" stop-color="#89b4fa" stop-opacity="0" />
</linearGradient>

Ahora los dos extremos tienen el mismo componente de color y solo varía el alfa, así que el matiz es constante en todo el recorrido.

Este es exactamente el mismo problema que en CSS produce la «franja gris» de un linear-gradient(blue, transparent), y que en CSS moderno se puede evitar con transparent porque el algoritmo cambió para interpolar en espacio premultiplicado. En SVG no ha cambiado: la interpolación de paradas sigue haciéndose sin premultiplicar, así que la regla del stop-opacity sigue siendo obligatoria.

Los degradados en SVG se interpolan en sRGB, y eso produce bandas donde CSS no las produce

Hay una segunda diferencia con CSS que explica por qué un mismo par de colores se ve peor en SVG. La propiedad color-interpolation controla el espacio en el que se mezclan las paradas, y su valor inicial es sRGB. El otro valor posible es linearRGB, que en teoría es más correcto físicamente y en la práctica produce degradados que parecen aclararse demasiado por el centro.

Ninguno de los dos es el espacio perceptualmente uniforme que hoy usarías. CSS moderno permite linear-gradient(in oklab, ...), que interpola en un espacio donde la luminosidad percibida varía de forma uniforme y no aparecen ni bandas ni zonas muertas. SVG no tiene equivalente: color-interpolation solo acepta auto, sRGB y linearRGB.

La consecuencia práctica: un degradado entre dos colores muy saturados y de matices lejanos (un azul intenso a un amarillo intenso) se ve claramente peor en SVG que en CSS, con una zona parda por el centro. Si el degradado es una pieza importante del diseño, hay dos salidas: interpolar tú en OKLab y emitir seis u ocho paradas intermedias calculadas, o poner el degradado en CSS sobre un elemento HTML y usar el SVG solo como máscara. La primera es la que funciona dentro de un SVG autocontenido, y no cuesta más que una función de veinte líneas ejecutada en tiempo de compilación.

⚔️ Reto práctico

Escribe una función que, dados dos colores y un número de paradas, genere la lista de stop interpolando en OKLab en lugar de en sRGB. Pinta el mismo par de colores con dos paradas y con tu versión de ocho, uno al lado del otro, y compara la zona central. Prueba con azul y amarillo, que es el par donde la diferencia es más brutal.