On this page
A gradient is an image generated from several inputs: colors, stops, an angle, and sometimes a center or shape. Animating the entire background-image value does not generally interpolate between arbitrary gradient declarations. A more controlled approach is to animate one typed input and let the browser rebuild the gradient from that changing value.
Start with a gradient worth keeping still
Build a static background with the gradient generator. Choose a small number of colors, copy the CSS, and preview it behind the actual content. The resting result must work before motion enters the design. A moving background cannot rescue a gradient whose brightest region makes labels disappear.
Choose a single animation variable: perhaps an angle rotating behind a badge, or a color stop drifting across a decorative separator. Moving several parameters at once makes debugging harder and often produces unnecessary visual activity. For a first implementation, use two colors and one angle.
Separate the decorative layer from readable content. If a heading sits over a gradient, place it on a stable opaque surface or verify every reachable background state. Use the contrast checker to check representative foreground/background pairs, including the lightest and darkest combinations the animation produces. Endpoint checks alone can miss a problematic intermediate blend.
Register an interpolable value
Unregistered custom properties behave like token sequences rather than typed angles or lengths. Their animation is generally discrete, so a variable can jump between values instead of moving smoothly. The @property registration tells the browser what kind of value it should interpolate.
A registration supplies a syntax descriptor, an inheritance decision, and, for this syntax, a valid initial value. An angle such as 0deg is computationally independent and works as an initial value. Avoid using a value such as 1em as the initial value for a registered length, because it depends on another computed value.
Read the MDN @property reference when choosing syntax. An angle registration should receive angles, not arbitrary strings. A percentage registration can drive a stop position, while a color registration can interpolate a color input. Match the grammar to the smallest useful parameter rather than allowing everything.
Names are shared across the document, so use a component-specific prefix. A generic variable called angle is easy for another component to reuse accidentally. Explicit inheritance also matters: a decorative component usually benefits from inherits: false, keeping its animated value from unexpectedly flowing into descendants.
Animate the angle with a static fallback
This example leaves the base gradient intact everywhere. Motion is opt-in through a class that JavaScript adds only when the registration API is available and the operating system has not requested reduced motion. CSS repeats the preference guard to handle preference changes safely.
@property --halo-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.halo {
--halo-angle: 0deg;
background: linear-gradient(
var(--halo-angle), #2447c7, #c23f8b
);
}
@media (prefers-reduced-motion: no-preference) {
.gradient-motion .halo {
animation: halo-turn 12s linear infinite;
}
}
@keyframes halo-turn {
from { --halo-angle: 0deg; }
to { --halo-angle: 360deg; }
}const canRegister =
"CSS" in window && "registerProperty" in CSS;
const motionAllowed = window.matchMedia(
"(prefers-reduced-motion: no-preference)"
).matches;
if (canRegister && motionAllowed) {
document.documentElement.classList.add("gradient-motion");
}The feature check is a practical enhancement gate for modern engines; test the actual stylesheet in your supported browsers as well. A custom-property support query alone would not prove that typed registration works. If JavaScript is unavailable, the static background remains complete and legible.
A full angle turn returns the image to an equivalent orientation, making this loop visually continuous. That does not mean any pair of gradient endpoints will loop cleanly. A stop moving from ten to ninety percent will jump when it resets unless the design hides the reset or uses alternating direction.
Control stops, pacing, and visual emphasis
For a moving stop, register a percentage and use that variable at a valid stop position. Keep neighboring stops ordered unless crossing them is intentional. When a later stop is placed before an earlier one, CSS gradient stop fixup can create a hard edge rather than the smooth wave you expected.
Use linear pacing for a seamless rotation. Ease-in-out is more useful when a decorative patch should drift to one side and return, because it reduces abrupt velocity changes at the ends. Neither choice makes the motion physically meaningful by itself; match the rhythm to the role of the surface.
Do not mistake a twelve-second loop for an accessibility guarantee. Slow, large-area movement can still distract or cause discomfort. Consider a single short entrance animation, a user-controlled preview, or an animated state limited to a loading task. Automatically moving content may also require pause controls depending on its behavior and context.
Keep motion out of error messages and reading surfaces unless it communicates something necessary. The micro-interaction guide offers a useful framing: the animation should reinforce a recognizable state change, not compete with the task. A decorative highlight that never stops has a very different attention cost from a brief confirmation.
Measure painting and verify fallbacks
Typed interpolation does not imply compositor-only animation. Changing gradient inputs commonly requires repainting the background. A small badge can be inexpensive while the same animation across a full viewport becomes costly. Compare both on a modest device, not only a development laptop.
Use paint flashing and a performance recording to see the affected area. The web.dev animation guide explains why transform and opacity are generally preferable when they can express the same effect. If rotating a separate decorative layer gives an acceptable result, compare that approach, while accounting for clipping and its different visual geometry.
Avoid stacking multiple animated gradients, filters, and backdrop blur before measuring. Isolate one effect, record a baseline, and add complexity only when the result justifies the cost. Large layers also have memory costs even when their animation can be composited.
Test progressive enhancement explicitly: disable JavaScript, remove the motion class, enable reduced motion, and inspect a browser without registration support. Every case should retain the intended static design. Consult MDN on CSS custom properties for variable fallback behavior, which is distinct from feature detection. Finally, verify the chosen angle and stop names do not collide with other components after stylesheets are combined.
Key takeaways
- Build a readable static gradient before animating any parameter.
- Register angles, percentages, or colors with the correct syntax.
- Animate a single input first and check loop continuity.
- Treat gradient animation as potential paint work, not a free effect.
- Keep unsupported and reduced-motion experiences complete without animation.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor