§ NOTAS TÉCNICAS aGo · 2026-05-24

Mesh gradients reales con conic-gradient

aGo CSS Gradient genera mesh gradients estilo Stripe, Linear o Apple sin imagen ni canvas, en CSS puro. Esta nota cuenta cómo lo logramos componiendo radial y conic gradients, y dónde el truco no alcanza para imitar un mesh "real" de Figma.

El contexto

Mesh gradients están de moda otra vez. Stripe los usa en hero, Linear los usa en marketing, Apple los usa para anclar mood de sus landings. Visualmente son manchas suaves de color que se mezclan sin bordes duros. Diseñador feliz, developer en problemas.

El developer típico recibe un PNG exportado de Figma con el mesh ya renderizado. El PNG pesa entre 200 KB y 1 MB, no escala (un hero a 4K se ve borroso), no es animable, no se adapta a tema claro/oscuro, y consume un request HTTP que bloquea LCP. Para una landing rápida es trade-off terrible.

aGo CSS Gradient resuelve el problema en la otra dirección: produce un CSS gradient que se acerca visualmente al mesh, escala infinito sin perder nitidez, es animable con keyframes, y pesa cero bytes adicionales sobre el HTML. El usuario lo edita en una UI con manchas arrastrables, copia el CSS resultante y lo pega en su sitio.

El constraint duro es estético: el resultado tiene que verse bien. Si el CSS gradient se ve menos pulido que el PNG de Figma, nadie lo va a usar. Por eso esta nota existe: encontrar la composición que produce un resultado visualmente competitivo.

Las opciones que evaluamos

1. SVG con filter y feGaussianBlur sobre círculos de color

Es la solución más directa. Pintas círculos de colores en un SVG, le aplicas un feGaussianBlur con stdDeviation alto, y obtienes manchas suaves que se mezclan. Stripe usa una variante de esto en su landing.

Ventajas: control granular, blend modes potentes, animable. Contras: el SVG con filtros pesa más que un CSS string, requiere markup HTML específico (no es un fondo "drop-in"), y los filtros gausianos en SVG sobre superficies grandes son más caros en GPU de lo que parece.

2. Canvas 2D rasterizado a una sola vez

Renderizar el mesh en canvas, exportar a PNG inline (base64) y usarlo como background-image. Producible, pero perdemos todas las ventajas que buscábamos: deja de ser escalable, deja de ser editable post-export, deja de ser animable.

3. Composición de radial-gradient + conic-gradient en CSS

La opción menos obvia. Apilas 3-5 capas de gradient con background y mix-blend-mode. Cada capa es una mancha de color con falloff radial o cónico. La superposición con blending produce mezcla suave que se acerca al mesh.

Ventajas: CSS puro (cero bytes), escalable, animable con keyframes sobre background-position, copiar/pegar drop-in. Contras: cada navegador compone blend modes con leves diferencias (Safari rinde un poco distinto a Chrome en darken/multiply), y no es mesh real con control points.

La decisión y por qué

Elegimos la composición CSS de radial y conic. Razones, en orden.

Primera: portabilidad del output. El usuario de aGo CSS Gradient quiere un string que pegue en su sitio y funcione. Un SVG con filtros requiere markup. Una imagen requiere asset hosting. Un CSS gradient se pega como valor de background y ya. Para un 70 % de los casos donde el mesh es decorativo, esto es suficiente.

Segunda: peso. La salida típica de Gradient pesa entre 200 y 600 bytes. Un PNG mesh exportado de Figma pesa 300 KB. La diferencia en LCP es notable, sobre todo en móvil con red 4G inestable.

Tercera: animación. background-position y background-size son propiedades baratas de animar en CSS. Un mesh CSS se puede hacer respirar suavemente con un keyframe de 20 segundos sin costo perceptible en CPU. Un PNG no puede.

Cuarta: tema oscuro. El gradient CSS puede usar variables de tema (var(--color-mesh-1)) y cambiar de paleta automáticamente cuando el usuario cambia de modo claro a oscuro. Un PNG requiere dos assets y switching JS.

Cómo lo implementamos

El núcleo de la generación es un mapper: dado un array de "stops" con posición, color, tamaño y opacidad, producir un string CSS multi-layer. Esto vive en el editor de Gradient y también se expone como utilidad standalone para quien copie el código.

// src/islands/css-gradient/lib/mesh.ts
// Genera un mesh gradient CSS componiendo radial y conic gradients.
// El resultado es un valor de 'background' valido directamente en CSS.

export interface MeshStop {
  /** Centro en porcentaje, ej. 20 = 20% desde la izquierda */
  x: number;
  /** Centro en porcentaje */
  y: number;
  /** Color en cualquier sintaxis CSS valida (hex, rgb, hsl, oklch) */
  color: string;
  /** Tamano de la mancha en porcentaje del lado largo. 40 = mancha radio ~40% */
  size: number;
  /** 0..1, alpha del centro de la mancha */
  opacity?: number;
  /** Si es 'conic', se renderiza con conic-gradient para crear remolino */
  shape?: 'radial' | 'conic';
  /** Solo para conic: angulo inicial en grados */
  conicFrom?: number;
}

export interface MeshOptions {
  /** Color base bajo todas las manchas */
  base: string;
  /** Lista de manchas, ordenadas de fondo a frente */
  stops: MeshStop[];
  /** Blend mode usado para apilar las capas */
  blendMode?: 'normal' | 'screen' | 'overlay' | 'soft-light' | 'multiply';
}

export function buildMeshBackground(opts: MeshOptions): string {
  const blend = opts.blendMode ?? 'screen';
  const layers: string[] = [];

  for (const stop of opts.stops) {
    const alpha = stop.opacity ?? 0.85;
    const center = withAlpha(stop.color, alpha);
    const fade = withAlpha(stop.color, 0);
    if (stop.shape === 'conic') {
      const from = stop.conicFrom ?? 0;
      layers.push(
        `conic-gradient(from ${from}deg at ${stop.x}% ${stop.y}%, ${center}, ${fade} ${stop.size}%, ${center} ${stop.size * 1.6}%, ${fade} ${stop.size * 2}%)`
      );
    } else {
      layers.push(
        `radial-gradient(circle at ${stop.x}% ${stop.y}%, ${center} 0%, ${fade} ${stop.size}%)`
      );
    }
  }

  // La declaracion final apila las capas con su blend mode.
  // 'background' acepta una lista separada por comas; cada capa es independiente.
  // El color base va al final como capa solida.
  const stack = layers.join(',\n  ');
  return [
    'background:',
    `  ${stack},\n  ${opts.base}`,
    `;background-blend-mode: ${repeatBlend(blend, opts.stops.length)};`
  ].join('\n');
}

function repeatBlend(mode: string, count: number): string {
  // La ultima capa (color base) siempre normal para no perder color de fondo.
  return [...Array(count).fill(mode), 'normal'].join(', ');
}

function withAlpha(color: string, alpha: number): string {
  // Soporta hex (#rrggbb), rgb(), hsl(), oklch().
  // Convertimos a sintaxis que soporte alpha cuando sea necesario.
  const a = clamp01(alpha);
  if (color.startsWith('#')) {
    const { r, g, b } = parseHex(color);
    return `rgba(${r}, ${g}, ${b}, ${a})`;
  }
  if (color.startsWith('oklch(')) {
    // oklch acepta alpha despues de slash: oklch(70% 0.15 25 / 0.5)
    return color.replace(/\)$/, ` / ${a})`);
  }
  if (color.startsWith('hsl(')) {
    return color.replace('hsl(', 'hsla(').replace(/\)$/, `, ${a})`);
  }
  if (color.startsWith('rgb(')) {
    return color.replace('rgb(', 'rgba(').replace(/\)$/, `, ${a})`);
  }
  return color;
}

function parseHex(hex: string): { r: number; g: number; b: number } {
  const h = hex.slice(1);
  const full = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
  return {
    r: parseInt(full.slice(0, 2), 16),
    g: parseInt(full.slice(2, 4), 16),
    b: parseInt(full.slice(4, 6), 16)
  };
}

function clamp01(n: number): number {
  return Math.max(0, Math.min(1, n));
}

// Ejemplo de uso productivo para un hero estilo Stripe.
export function presetStripeHero(): MeshOptions {
  return {
    base: '#0B1F33',
    blendMode: 'screen',
    stops: [
      { x: 18, y: 30, color: '#FF7A3C', size: 55, opacity: 0.85, shape: 'radial' },
      { x: 80, y: 25, color: '#7C3AED', size: 50, opacity: 0.8, shape: 'radial' },
      { x: 50, y: 80, color: '#0EA5E9', size: 65, opacity: 0.75, shape: 'radial' },
      { x: 90, y: 90, color: '#F4EFE6', size: 35, opacity: 0.4, shape: 'conic', conicFrom: 45 }
    ]
  };
}

Tres piezas clave merecen comentario. Primera, la sintaxis del fade: para cada mancha generamos un gradient de "color con alpha alto en el centro, mismo color con alpha 0 en el borde". Eso produce el falloff suave que define al mesh. Sin alpha en el extremo, las manchas tienen borde duro y arruinan el efecto.

Segunda, background-blend-mode: screen en colores oscuros sobre fondo oscuro produce el efecto "luz" típico de Stripe. Si el fondo es claro, multiply o soft-light dan mejor resultado. Lo expusimos como switch en la UI.

Tercera, la capa conic con doble pulso (centro, transparente, centro, transparente) crea un patrón en remolino que rompe la perfección radial y agrega textura. Es el truco que separa un mesh CSS aceptable de uno que parece de stock.

Limitaciones que descubrimos

Cada navegador rinde blend modes con pequeñas diferencias. Chrome y Firefox producen mezcla casi idéntica en screen y multiply. Safari (incluso WebKit 17) da resultados un poco más opacos en overlay y soft-light. Si tu mesh depende fuertemente de overlay, vas a ver que en Safari se ve un 10 % distinto. Lo asumimos. Para corrección crítica habría que meter media queries por user-agent, cosa que evitamos.

No es mesh real. Un mesh gradient real, como el de SVG 2 mesh element o el de Figma, tiene control points en una grilla y produce interpolación entre vértices. Lo nuestro es composición de manchas, que aproxima visualmente pero no permite el control fino de "este punto va a este color exacto". Si necesitas eso, ningún CSS puro lo resuelve hoy.

Las capas con conic-gradient son más caras en GPU que las radiales. En un hero a pantalla completa con cuatro capas de conic, en un Android medio, la animación de background-position cae a 30-40 fps. Limitamos a una capa conic por mesh en los presets que ofrecemos.

Cuando el usuario tiene "reduce motion" habilitado, las animaciones de mesh se vuelven distractoras o nauseantes. Generamos el CSS con una media query @media (prefers-reduced-motion: reduce) que detiene la animación. Lo incluimos por defecto en el output.

Cuándo NO uses este approach

Si el diseño exige reproducir píxel a píxel un mesh que viene de Figma o Sketch, no lo intentes con CSS. Exportá el PNG, optimízalo agresivamente con AVIF o WebP, y úsalo como background-image. Aceptás el costo de un asset, ganás precisión.

Si necesitás control points editables en runtime por el usuario final, el approach CSS no lo permite. Para esa interacción se usa canvas o WebGL con un shader que pinte un mesh real. Ahí el peso del runtime se justifica porque el output es el editor mismo.

Si tu marca tiene un gradient específico que es activo de identidad (no decorativo de fondo), conviene tratarlo como activo de marca y exportar imagen para garantizar consistencia entre navegadores. Aproximar puede traicionar la marca.

Esta nota describe parte de cómo construimos aGo CSS Gradient. Abre Gradient y arrastra manchas para ver el approach en vivo.

¿Necesitas algo parecido para tu producto? aGo lab construye software cliente puro para empresas. Conversemos.

§ CÓMO CITAR ESTE ARTÍCULO

aGo lab. (2026). "Conic gradient mesh, trucos para fondos sofisticados". tools.ago.cl/notas/conic-gradient-mesh. Recuperado el 2026-05-25.

Esta nota es contenido propio de aGo lab y se publica bajo política de atribución requerida. Si la citas o referencias, incluye el link a la URL original.

§ aGo lab estudio

¿Quieres que aGo lab implemente esto para ti?

Si esta decisión técnica te resuelve un problema y necesitas aplicarla a un sistema propio, conversemos.

§ COMMAND

↑↓ navega · Enter abre · Esc cierra