The short version
- A CSS gradient hover effect cannot be made by transitioning background-image, because browsers swap gradients instantly rather than interpolating them.
- The most compatible technique is an oversized gradient with a transition on background-position, which slides new colours into view.
- Registering colour stops or angles with @property lets the browser transition them smoothly, and works in all current major browsers.
- Fading the opacity of a second gradient on a pseudo-element is the cheapest technique to render, because opacity changes do not repaint.
- Hover effects should sit inside a hover media query and respect prefers-reduced-motion.
On this page
A CSS gradient hover effect needs a workaround, because browsers don't transition background-image: change the gradient in :hover and it snaps. Four techniques give a smooth result. Slide an oversized gradient with background-position, animate registered colour stops with @property, crossfade a second gradient on a pseudo-element, or move a radial glow with the cursor. Each suits different elements, and all of them are below with code.
Why transition doesn't work on gradients
To the browser, a gradient is an image. It knows how to interpolate a number or a colour, but not one image into another, so background-image is not interpolated and the new gradient replaces the old one instantly. Custom properties used inside the gradient behave the same way unless you register them, because an unregistered property is just text.
/* This snaps, with no easing */
.btn {
background-image: linear-gradient(135deg, #1e1b4b, #6d28d9);
transition: background-image 400ms ease;
}
.btn:hover {
background-image: linear-gradient(135deg, #6d28d9, #f472b6);
}Technique 1: slide an oversized gradient
Make the background larger than the element and paint both states into it, then move it. background-position can be transitioned, so the new colours glide in. It works in every browser and needs no registration.
.btn {
background-image: linear-gradient(90deg, #1e1b4b 0%, #6d28d9 50%, #f472b6 100%);
background-size: 200% 100%;
background-position: 0% 50%;
transition: background-position 500ms ease;
}
.btn:hover {
background-position: 100% 50%;
}linear-gradient(90deg, #1e1b4b 0%, #6d28d9 50%, #f472b6 100%)The movement reads as a sweep, which suits buttons and links. It is less convincing on large cards, where you can see the gradient travel. Animating background-position repaints, which is fine for small elements; for a big surface, use technique 3. Gradient buttons in CSS has more button patterns built on this.
Technique 2: animate colour stops with @property
Register the stop colours as typed custom properties and the browser can interpolate them. The gradient then changes colour in place, with no sliding. @property works in all current major browsers; older ones simply snap, which is an acceptable fallback for a hover. The registration rules are covered in CSS @property.
@property --stop-a {
syntax: "<color>";
inherits: false;
initial-value: #1e1b4b;
}
@property --stop-b {
syntax: "<color>";
inherits: false;
initial-value: #6d28d9;
}
.card {
background-image: linear-gradient(135deg, var(--stop-a), var(--stop-b));
transition: --stop-a 450ms ease, --stop-b 450ms ease;
}
.card:hover {
--stop-a: #6d28d9;
--stop-b: #f472b6;
}The same trick animates the angle. Register --angle with syntax: "<angle>", use it in linear-gradient(var(--angle), ...) and change it on hover: a gradient that turns from 135 to 225 degrees as the pointer arrives is a subtle, polished touch for cards and gradient borders.
Both techniques so far also work on gradient text. Text clipped with background-clip: text shows whatever the background does, so a slide or a stop transition on a heading makes the colour run through the letters on hover. Keep the change gentle on headings people need to read, and make sure both states meet contrast against the page.
Technique 3: crossfade two gradients
Put the hover gradient on a pseudo-element above the base and fade its opacity. Opacity changes are handled by the compositor without repainting, so this is the smoothest choice for large cards, hero panels and anything in a long grid.
.tile {
position: relative;
isolation: isolate;
background-image: linear-gradient(135deg, #1e1b4b, #6d28d9);
}
.tile::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background-image: linear-gradient(135deg, #6d28d9, #f472b6);
opacity: 0;
transition: opacity 400ms ease;
}
.tile:hover::before {
opacity: 1;
}z-index: -1 with isolation: isolate keeps the pseudo-element behind the tile's text but above its base gradient.Technique 4: a glow that follows the cursor
A radial gradient positioned with custom properties can track the pointer. Set --x and --y from a small script and the light moves with the hand. Keep it soft and keep it away from text: it should feel like light on a surface, never a halo around words.
.panel {
background-image:
radial-gradient(circle 220px at var(--x, 50%) var(--y, 50%), rgb(244 114 182 / 0.35), transparent 70%),
linear-gradient(135deg, #1e1b4b, #312e81);
}
/* JavaScript:
panel.addEventListener("pointermove", (e) => {
const r = panel.getBoundingClientRect();
panel.style.setProperty("--x", `${e.clientX - r.left}px`);
panel.style.setProperty("--y", `${e.clientY - r.top}px`);
});
*/Choosing a technique
Looks like
Rendering cost
Best for
Looks like
Rendering cost
Best for
Looks like
Rendering cost
Best for
Looks like
Rendering cost
Best for
For gradients that move on their own rather than on hover, see CSS animated gradients. For the wider picture of what costs paint time, see CSS gradient performance.
Touch screens and reduced motion
Avoid
- Hover styles that stick after a tap on phones
- Information that only appears on hover
- Long, looping hover animations
- Ignoring the reduced motion setting
Do
- Wrap hover rules in
@media (hover: hover) - Show the same state on
:activefor touch - Keep transitions between 200 and 500ms
- Cut movement under
prefers-reduced-motion, keep the colour change
@media (hover: hover) {
.btn:hover { background-position: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.btn { transition: none; }
}Hover effects bring a website to life. On social posts there is no pointer, so motion has to live in the design itself. Gradiently's Marks can carry their own gentle motion, and with Pro a design exports as MP4, WebM or GIF where the browser supports recording, ready for a story or a post.
Questions people ask
Can you transition a CSS gradient on hover?
Not directly. background-image changes instantly. Transition background-position on an oversized gradient, animate registered stops with @property, or fade a second gradient's opacity.
How do I make a gradient button change colour smoothly on hover?
Use background-size: 200% 100% with a three stop gradient, then transition background-position from 0% 50% to 100% 50% on hover.
Does @property work in all browsers?
It works in current versions of Chrome, Edge, Safari and Firefox. Older browsers snap between states instead of easing.
What is the most performant gradient hover effect?
An opacity crossfade: the hover gradient sits on a pseudo-element and only its opacity changes, which avoids repainting.
Should gradient hover effects respect reduced motion?
Yes. Under prefers-reduced-motion: reduce, remove the movement and keep a simple colour change.
Written by Gradiently
The team behind Gradiently, a design tool built around Marks: living gradients that make everything you design look like yours.
See our profile