Skip to content

Gradient Enhancement Research

Research document for upgrading the gradient system from basic linear-only to a more capable, Figma-inspired implementation.

Current State

Data Model

AF type (Blocks/data/types.ts:214):

typescript
export type BackgroundSettings =
  | { mode: 'none' }
  | { mode: 'color'; color: string }
  | { mode: 'gradient'; colors: string[] }
  | { mode: 'image'; url: string; imageName: string; size: ObjectFit; enableManualUrl: boolean }

CE type (interfaces/jsonTypes/payload-v2/index.ts:10):

typescript
export type BackgroundSettings =
  | { mode: 'gradient'; colors: string[] }
  | { mode: 'image'; url: string; size: string }
  | { mode: 'color'; color: string }
  | { mode: 'none' }

Defaults (defaults.ts:331):

typescript
export const backgroundGradientDefaults = (
  colors: string[] = ['#00C3FFFF', '#30AFD6FF']
): BackgroundSettings => ({
  colors,
  mode: 'gradient',
})

Limitations

AspectCurrentDesired
Gradient typeLinear onlyLinear, radial, conic
DirectionHardcoded to bottomUser-configurable angle
ColorsExactly 22+ color stops
Stop positionsNone (even distribution)Per-stop percentage
PreviewBasic linear previewReflects actual gradient

Rendering

CE (styles/components/helpers.ts:159-162):

typescript
if (mode === 'gradient') {
  return {
    backgroundImage: `linear-gradient(to bottom, ${colors[0]}, ${colors[1]})`,
  }
}

AF preview (ImagePreview.vue:64-68):

typescript
generateGradient() {
  const from = this.gradient[0]
  const to = this.gradient[1]
  return `background: linear-gradient(${from}, ${to})`
}

Usage

BackgroundStyleSection is used in 15+ configuration files, all with identical event pattern:

vue
<BackgroundStyleSection
  :background-settings="blockData.backgroundSettings"
  @update:active="updateValue($event, 'background')"
  @update:settings="updateValue($event, 'backgroundSettings')"
  @update:color="updateValue($event, 'backgroundSettings.color')"
  @update:gradient="updateValue($event, 'backgroundSettings.colors')"
  @update:image="imageUploaded"
/>

The @update:gradient event currently emits string[] (2-element array). This will need to change to support richer gradient data.


Reusable Components

Already exist and can be reused directly

ComponentLocationReuse for
ColorInputcomponents/common/ColorInput.vueColor stop pickers (supports alpha)
RotationSectionConfiguration/components/RotationSection.vueAngle input pattern (InputField number-input + "deg" suffix)
OptionList + OptionButtonConfiguration/components/OptionList.vueGradient type selector (linear/radial/conic)
ImagePreviewcomponents/common/ImagePreview.vueGradient preview (needs update to render new types)
InputFieldcomponents/common/InputField.vueNumber inputs for angle, stop positions
sectionLogic mixinConfiguration/mixins/sectionLogic.tsOverride system integration

Border per-side pattern as reference

BorderStyleSection handles unified vs per-side modes via a perSide boolean. The same pattern could inspire a "simple vs advanced" toggle for gradient stops.


CSS Gradient Types

What CSS supports natively

css
/* Linear - direction via angle or keywords */
linear-gradient(135deg, #FF60FE, #5758D5)
linear-gradient(to top right, #FF60FE, #5758D5)

/* Radial - shape + position */
radial-gradient(circle at center, #FF60FE, #5758D5)
radial-gradient(ellipse at 30% 70%, #FF60FE, #5758D5)

/* Conic - angle + position */
conic-gradient(from 45deg at 50% 50%, #FF60FE, #5758D5)

/* Multi-stop with explicit positions */
linear-gradient(135deg, #FF60FE 0%, #5758D5 50%, #00C3FF 100%)

What Figma supports

TypeCSS equivalentComplexity
Linearlinear-gradient()Low - angle + stops
Radialradial-gradient()Medium - position + shape
Angularconic-gradient()Medium - start angle + position
DiamondNo CSS equivalentHigh - requires SVG or multiple gradients

Recommendation: Skip diamond. It has no native CSS support and would require complex workarounds (rotated element with radial gradient, or SVG). Linear, radial, and conic cover 99% of use cases.


Proposed Data Model

typescript
// -- Color stops --

type GradientStop = {
  color: string    // hex with alpha, e.g. '#FF60FEFF'
  position: number // 0-100 percentage
}

// -- Gradient settings (new) --

type GradientSettings = {
  type: 'linear' | 'radial' | 'conic'
  angle: number           // degrees, used by linear + conic
  stops: GradientStop[]   // 2+ stops
  // Radial-specific
  shape?: 'circle' | 'ellipse'
  position?: { x: number; y: number } // percentage, default 50/50
}

// -- BackgroundSettings (updated) --

export type BackgroundSettings =
  | { mode: 'none' }
  | { mode: 'color'; color: string }
  | { mode: 'gradient'; colors: string[]; gradient?: GradientSettings }
  | { mode: 'image'; ... }

Backwards compatibility

The colors array is kept for backwards compatibility with existing creatives. The engine checks for gradient first, falls back to colors:

typescript
if (mode === 'gradient') {
  if (settings.gradient) {
    return renderGradient(settings.gradient)
  }
  // Legacy fallback
  return {
    backgroundImage: `linear-gradient(to bottom, ${colors[0]}, ${colors[1]})`,
  }
}

New creatives always write both colors (first two stop colors, for legacy previews) and gradient (full settings).

Default values

typescript
export const gradientSettingsDefaults = (): GradientSettings => ({
  type: 'linear',
  angle: 180,
  stops: [
    { color: '#00C3FFFF', position: 0 },
    { color: '#30AFD6FF', position: 100 },
  ],
})

Rendering (CE)

CSS generation helper

typescript
const renderGradient = (gradient: GradientSettings): Styles => {
  const stops = gradient.stops
    .map((s) => `${s.color} ${s.position}%`)
    .join(', ')

  switch (gradient.type) {
    case 'linear':
      return { backgroundImage: `linear-gradient(${gradient.angle}deg, ${stops})` }

    case 'radial': {
      const shape = gradient.shape || 'ellipse'
      const pos = gradient.position || { x: 50, y: 50 }
      return { backgroundImage: `radial-gradient(${shape} at ${pos.x}% ${pos.y}%, ${stops})` }
    }

    case 'conic': {
      const pos = gradient.position || { x: 50, y: 50 }
      return { backgroundImage: `conic-gradient(from ${gradient.angle}deg at ${pos.x}% ${pos.y}%, ${stops})` }
    }
  }
}

UI Design (AF)

BackgroundStyleSection changes

When gradient mode is selected, the section expands to show:

[None] [Color] [Gradient] [Image]

  Type:  [Linear] [Radial] [Conic]

  Angle: [180] deg              (linear + conic only)

  Position: [50] % [50] %       (radial + conic only)

  Shape: [Circle] [Ellipse]     (radial only)

  Stops:
    [ColorInput] ---- [0] %
    [ColorInput] ---- [100] %
    [+ Add stop]

  [====== Preview ======]

Component breakdown

  1. Gradient type selector - OptionList with 3 buttons, already exists as pattern
  2. Angle input - Reuse RotationSection pattern (InputField + "deg")
  3. Position inputs - Two InputField number-inputs with "%" suffix (radial/conic)
  4. Shape selector - OptionList with 2 buttons (radial only)
  5. Color stops - List of ColorInput + position InputField pairs, with add/remove
  6. Preview - Updated ImagePreview that renders all gradient types

Conditional visibility

ControlLinearRadialConic
AngleYesNoYes
PositionNoYesYes
ShapeNoYesNo
StopsYesYesYes

ImagePreview update

ImagePreview needs a gradientSettings prop (or the existing gradient prop changes shape). It generates the CSS string the same way the engine does:

typescript
generateGradient() {
  if (this.gradientSettings) {
    return `background: ${renderGradientCSS(this.gradientSettings)}`
  }
  // Legacy
  return `background: linear-gradient(${this.gradient[0]}, ${this.gradient[1]})`
}

Scope

All features ship together in a single branch:

  • Gradient type selector (linear/radial/conic)
  • Circular angle dial (linear + conic)
  • Unlimited color stops with position control
  • Visual gradient bar with draggable stop handles
  • Radial shape selector (circle/ellipse)
  • Visual position picker for radial/conic (click on preview)
  • Updated preview and engine rendering
  • Backwards-compatible with existing creatives

Files to Change

Application-Frontend

FileChange
Blocks/data/types.tsAdd GradientStop, GradientSettings types, update BackgroundSettings
Blocks/data/defaults.tsAdd gradientSettingsDefaults(), update backgroundGradientDefaults()
Configuration/components/BackgroundStyleSection.vueAdd type/angle/stops UI
components/common/ImagePreview.vueSupport new gradient types in preview
assets/i18n/en.jsAdd keys for linear, radial, conic, angle, position, shape, stops
Blocks/utils.tsVerify sectionSettings registration (already covers backgroundSettings)

Creative-Engine

FileChange
interfaces/jsonTypes/payload-v2/index.tsAdd GradientStop, GradientSettings, update BackgroundSettings
styles/components/helpers.tsAdd renderGradient(), update getBackgroundProperties()

No changes needed

  • Configuration files that use BackgroundStyleSection (they pass the full backgroundSettings object)
  • Vuex store (uses generic updateBlockValue mutation)
  • configurationLogic mixin (generic path-based updates)
  • sectionSettings registry (already tracks backgroundSettings)

Decisions

  1. Type selector inside gradient mode -- select Gradient first, then choose type (linear/radial/conic). Not top-level.
  2. Unlimited color stops -- no artificial limit.
  3. Circular dial for angle -- visual, Figma-style.
  4. Visual position picker -- click on preview to set center point for radial/conic.
  5. All in one go -- no phasing, ship everything together.

Internal documentation