# CSS gradient hover effect: four ways to animate it

[Canonical HTML page](https://gradiently.design/guide/css-hover-gradient)

Write a new gradient in :hover and it snaps instead of easing. Browsers can't transition one gradient into another, but they can move one, fade one or animate the colours inside it. Here is each method, with the code.

## 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.

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.

```css
/* 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);
}
```

The transition line is ignored for gradients. Every technique below works around this.

- Resting state: `linear-gradient(135deg, #1e1b4b 0%, #6d28d9 100%)`
- Hover state: `linear-gradient(135deg, #6d28d9 0%, #f472b6 100%)`

The two states used in the examples. The goal is a smooth journey from the left swatch to the right one.

## 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.

```css
.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%;
}
```

At rest you see the left half, indigo to violet. On hover the right half, violet to pink, slides into place.

The full 200% strip: `linear-gradient(90deg, #1e1b4b 0%, #6d28d9 50%, #f472b6 100%)`

This is the whole background. The button only ever shows half of it at a time.

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](https://gradiently.design/guide/css-gradient-button) 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](https://gradiently.design/guide/css-property-animation).

```css
@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;
}
```

Each stop eases independently. Give them different durations and the colour seems to flow from one corner to the other.

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](https://gradiently.design/guide/css-gradient-border).

Both techniques so far also work on [gradient text](https://gradiently.design/guide/css-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.

```css
.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.

```css
.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`);
   });
*/
```

The glow is a top layer over a dark base. Updating two custom properties is cheap, but each move repaints, so use it on a few panels, not every card on a page.

## Choosing a technique

| Technique | Looks like | Rendering cost | Best for |
| --- | --- | --- | --- |
| Slide with background-position | A sweep of new colour | Repaints during the transition | Buttons, links, tags |
| @property colour stops | Colours shifting in place | Repaints during the transition | Cards, badges, borders |
| Opacity crossfade | One gradient dissolving into another | Composited, no repaint | Large panels, long grids |
| Cursor glow | Light following the pointer | Repaints on every move | A few feature panels |

When in doubt, crossfade. It is the cheapest and works everywhere.

For gradients that move on their own rather than on hover, see [CSS animated gradients](https://gradiently.design/guide/css-animated-gradient). For the wider picture of what costs paint time, see [CSS gradient performance](https://gradiently.design/guide/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 `:active` for touch
- Keep transitions between 200 and 500ms
- Cut movement under `prefers-reduced-motion`, keep the colour change

```css
@media (hover: hover) {
  .btn:hover { background-position: 100% 50%; }
}

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
}
```

The colour still changes for everyone; only the movement is removed for people who asked for less.

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.

## FAQ

### 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.
