Frontend Visual Gradients
This skill defines deterministic rules for shipping CSS gradients (linear-gradient, radial-gradient, conic-gradient, their repeating-* variants), choosing the right colour-interpolation space to avoid muddy midpoints, animating gradients via typed custom properties, emulating mesh gradients with layered radials, and gating animated gradients for motion sensitivity. CSS gradients have been Baseline Widely Available since July 2015; the in <colorspace> interpolation hint (with polar hue arc modifiers) is Baseline 2024.
This skill builds on [[frontend-syntax-css-color-modern]] (oklch, color-mix, relative-colour) and [[frontend-theming-color-palette-oklch]] (palette shades to feed into gradients). Animation primitives come from [[frontend-perf-animation-gpu-containment]] (compositor-only rule + @property).
Sources : MDN: linear-gradient() (verified 2026-05-19), MDN: conic-gradient() (verified 2026-05-19), MDN: <gradient> (verified 2026-05-19), W3C: CSS Images Module Level 4 (verified 2026-05-19), MDN: @property (verified 2026-05-19).
Quick Reference
Gradient functions
| Function | Direction control | Use when |
|---|---|---|
linear-gradient(<dir>, <stops>) |
to <side-or-corner> keyword OR <angle> (0deg = to top, 90deg = to right) |
Two-colour across an axis, simple hero backgrounds, accent strips. |
radial-gradient(<shape> at <pos>, <stops>) |
circle or ellipse, optional <size> keyword, at <position> |
Spot highlights, vignettes, soft glows, mesh-emulation building blocks. |
conic-gradient([from <angle>] [at <position>], <stops>) |
from <angle> (default 0deg = top); at <position> (default center) |
Pie charts, loading rings, colour wheels, sweep effects. |
repeating-linear-gradient(...) |
same as linear | Diagonal stripes, ribbons, ruled paper. |
repeating-radial-gradient(...) |
same as radial | Concentric rings. |
repeating-conic-gradient(...) |
same as conic | Checkerboards, slice patterns. |
Stops cheat-sheet
| Form | Example | Meaning |
|---|---|---|
| Implicit positions | linear-gradient(red, blue) |
Stops evenly distributed. |
| Explicit positions | linear-gradient(red 0%, yellow 50%, blue 100%) |
Authoritative positions. |
| Hard stop (two positions on one stop) | linear-gradient(red 0 50%, blue 50% 100%) |
Sharp transition at 50% (creates a stripe). |
| Midpoint hint | linear-gradient(red, 30%, blue) |
Pulls the midpoint between the two colours to the 30% mark. |
| Repeating with hard stops | repeating-linear-gradient(45deg, #ccc 0 10px, #fff 10px 20px) |
Diagonal stripe pattern, 10px wide. |
Colour-interpolation hint (Baseline 2024)
/* default = sRGB; OFTEN muddy at midpoints */
linear-gradient(to right, blue, yellow)
/* perceptually-uniform interpolation; cleaner midpoint */
linear-gradient(in oklch to right, blue, yellow)
/* polar space with hue arc selection */
linear-gradient(in oklch longer hue, red, lime)
| Form | Use |
|---|---|
in oklab |
Rectangular perceptual; best general-purpose default for tints / shades / two-colour blends. |
in oklch |
Polar perceptual; required when you also want a hue-arc modifier. |
in srgb |
The legacy default. AVOID unless intentionally targeting the legacy mid-grey effect. |
in lab / in lch |
Older perceptual spaces; OKLCH/OKLab preferred per MDN: linear-gradient() (verified 2026-05-19). |
in hsl / in hwb |
Hue-driven legacy spaces; useful for rainbow gradients where the cylindrical model is desired. |
Hue-interpolation modifiers (polar spaces only) :
| Modifier | Effect |
|---|---|
shorter hue (default) |
Travel via the shortest arc between hues. |
longer hue |
Travel via the longer arc (full-spectrum rainbows). |
increasing hue |
Always increasing hue angle. |
decreasing hue |
Always decreasing hue angle. |
Animated gradients via @property
@property --grad-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.hero {
background: linear-gradient(var(--grad-angle), #4d4dff, #ff66cc);
background-size: 200% 200%;
animation: spin 12s linear infinite;
}
@keyframes spin { to { --grad-angle: 360deg; } }
Without @property registration the custom property is interpolation-opaque and the angle would snap at the end of each cycle (MDN: @property (verified 2026-05-19), Baseline 2024).
ALWAYS gate animated gradients behind prefers-reduced-motion :
@media (prefers-reduced-motion: reduce) {
.hero { animation: none; }
}
Mesh gradient (emulation)
CSS has no native mesh-gradient function. Stack multiple radial gradients :
.mesh {
background:
radial-gradient(circle at 20% 30%, oklch(0.72 0.18 320) 0%, transparent 50%),
radial-gradient(circle at 80% 40%, oklch(0.72 0.18 220) 0%, transparent 55%),
radial-gradient(circle at 50% 80%, oklch(0.72 0.18 110) 0%, transparent 60%),
oklch(0.95 0.02 240);
}
Each radial is positioned at a different point, fades to transparent, and stacks over a solid base. The visible result reads as a four-colour mesh blend.
Decision Trees
Decision : linear, radial, or conic?
What shape does the colour transition follow?
A line (across, diagonal, top to bottom)
-> linear-gradient(<dir>, ...)
Add `in oklch` or `in oklab` for clean midpoints.
Outward from a point (spotlight, vignette, glow)
-> radial-gradient([shape] at <pos>, ...)
`circle` for a perfect circle; `ellipse` (default) follows
the box's aspect ratio.
Sweeping around a centre point (pie chart, loading ring, colour wheel)
-> conic-gradient([from <angle>] [at <pos>], ...)
Stops expressed in degrees or percentages.
Multiple radial "blobs" blending together (mesh-style)
-> Stack multiple radial-gradient layers on the same element
with transparency fade-outs and an underlying solid base.
A repeating geometric pattern (stripes, checkerboard, grid)
-> repeating-linear-gradient / repeating-conic-gradient with
hard stops.
Decision : why does my gradient look muddy or banded?
What does the bad result look like?
Midpoint is grey, beige, or dull when the endpoints are vivid
complementary colours (blue -> yellow muddies through olive-grey).
-> Switch interpolation space to oklab or oklch :
linear-gradient(in oklab to right, blue, yellow);
Visible stripes parallel to the gradient direction on a large
surface (banding).
-> Two causes : (a) too few colour stops on a long surface;
(b) sRGB interpolation. Both are mitigated by switching
to `in oklch`. Adding more stops alone WITHOUT changing
interpolation rarely fixes it.
Hue takes the "wrong" path (red to green going via brown instead
of via the bright spectrum).
-> Use a polar space + hue arc :
linear-gradient(in oklch longer hue, red, green);
Hard-edge appears where the author wanted a smooth blend.
-> A stop pair sharing the same position creates a hard edge.
Use distinct positions (red 0%, blue 100%) instead of
(red 50%, blue 50%).
Decision : static or animated gradient?
Does the gradient change over time or in response to interaction?
No, it is static.
-> Plain background-image rule. Pick the right interpolation
space and ship it. Set on background-image; never on
background-color (gradients are images).
Yes, the gradient rotates (angle changes).
-> Register --grad-angle with @property as <angle>; declare
the gradient with var(--grad-angle); animate the custom
property. Gate with prefers-reduced-motion.
Yes, the gradient shifts colour (tint shift, hue drift).
-> Register the colour custom property with @property as <color>;
animate the custom property. Gate with prefers-reduced-motion.
Yes, the gradient "moves" across the surface (animated band).
-> Use background-size > 100% and animate background-position.
NOTE : background-position animation triggers paint per frame
on the entire painted region; budget paint cost OR replace
with a transformed overlay and animate transform.
Decision : mesh approximation strategy?
How complex is the target mesh?
2-4 blobs, soft edges, no sharp boundaries.
-> Stack 2-4 radial-gradient layers on one element, each fading
to transparent over a solid base colour. See Pattern 4 in
examples.md.
Many irregular blobs, organic shapes.
-> SVG <feGaussianBlur> on randomised <circle> elements is the
right tool; reference the SVG as a CSS background-image
(url(#mesh-filter)) or render inline. Out of scope for this
skill; see Cross-references.
Animated mesh (blobs drift slowly).
-> Two compositor-friendly options :
1. Animate transforms on absolutely-positioned blurred divs
layered over a base (movement is on transform, not paint).
2. Animate a typed custom property used inside the radial
gradient (e.g. --blob1-x : <length>). Costs paint per
frame; budget accordingly.
Patterns
Pattern 1 : smooth two-colour hero with in oklch
.hero { background: linear-gradient(in oklch to right, oklch(0.65 0.18 250), oklch(0.72 0.20 320)); }
Vivid endpoints, no muddy midpoint, no banding on a 1920px-wide surface.
Pattern 2 : conic loading ring
.spinner {
width: 4rem; height: 4rem; border-radius: 50%;
background: conic-gradient(from 0deg, transparent 0deg, var(--brand-600) 270deg, transparent 270deg);
animation: rotate 1s linear infinite;
}
@keyframes rotate { to { transform: rotate(360deg); } }
Animation is on transform (composite-only) NOT on the gradient itself; the gradient is static.
Pattern 3 : repeating diagonal stripes
.stripes { background: repeating-linear-gradient(45deg, #eee 0 10px, #fff 10px 20px); }
Two hard-stop pairs make a 10px stripe + 10px gap; the pattern repeats automatically because the gradient is repeating-linear-gradient.
Pattern 4 : mesh emulation with layered radials
.mesh {
background:
radial-gradient(circle at 20% 25%, oklch(0.78 0.18 320) 0%, transparent 50%),
radial-gradient(circle at 80% 30%, oklch(0.78 0.18 220) 0%, transparent 55%),
radial-gradient(circle at 65% 75%, oklch(0.78 0.18 130) 0%, transparent 60%),
oklch(0.95 0.02 240);
}
See examples.md for a renderable HTML demo.
Pattern 5 : animated gradient angle with @property
@property --grad-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
.hero {
background: linear-gradient(var(--grad-angle), oklch(0.65 0.18 250), oklch(0.72 0.20 320));
background-size: 200% 200%;
animation: rotate-grad 14s linear infinite;
}
@keyframes rotate-grad { to { --grad-angle: 360deg; } }
@media (prefers-reduced-motion: reduce) { .hero { animation: none; } }
Pattern 6 : gradient on text
.headline {
background: linear-gradient(in oklch 90deg, oklch(0.6 0.2 250), oklch(0.65 0.22 320));
background-clip: text;
color: transparent;
/* Safari prefix shim for some older versions; harmless if dropped: */
-webkit-background-clip: text;
}
Provide a fallback color via @supports not (background-clip: text) if a baseline-compatible look matters on older engines.
Anti-Patterns Index
See anti-patterns.md. Eight cataloged : default sRGB interpolation midpoint mud; untyped gradient custom property snap; background-attachment: fixed parallax on mobile; 30-stop "smoothing" gradient; conic pie chart without role="img"; animated gradient without prefers-reduced-motion; background-position animation paint storm; gradient set on background-color (does nothing).
Reference Links
- Methods and signatures : function syntax, interpolation methods, hue arc modifiers, stop forms, gradient layering rules, performance notes.
- Examples : renderable HTML demo with linear, radial, conic, animated, and mesh-emulation gradients side by side.
- Anti-patterns : eight cataloged anti-patterns with symptom, root cause, and fix.
Cross-references
[[frontend-syntax-css-color-modern]]:oklch(),color-mix(),light-dark(), full modern colour syntax.[[frontend-theming-color-palette-oklch]]: palette shades that feed brand-aware gradients.[[frontend-perf-animation-gpu-containment]]: compositor-only rule and@propertyfor animatable customs.[[frontend-visual-glassmorphism-backdrop]]:backdrop-filterlayered over a gradient surface.[[frontend-a11y-motion-contrast-wcag22]]:prefers-reduced-motionand contrast requirements for text on gradient surfaces.