# CSS @property: animating gradients and custom properties

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

Gradients refuse to transition because the browser cannot blend one image into another. Give a custom property a type and that limit disappears. Here is how @property works, with code you can paste.

## The short version

- CSS @property registers a custom property with a syntax, an inheritance rule and an initial value, which tells the browser what type of value it holds.
- Browsers cannot transition a gradient directly, because background-image is not interpolable, but they can transition a typed custom property used inside the gradient.
- Typed properties such as <color>, <angle>, <percentage> and <length> animate smoothly; a property with the universal * syntax only flips between values.
- The @property rule is supported in current Chrome, Edge, Safari and Firefox, and browsers that ignore it simply show the gradient without motion.
- Animating a custom property inside a background repaints the element on every frame, so keep the animated area modest and respect prefers-reduced-motion.

**CSS @property** registers a custom property with a type, so the browser knows that `--angle` holds an angle or `--tint` holds a colour. Once it knows the type, it can interpolate between two values, which means you can transition and animate things CSS normally refuses to move: gradient colours, gradient angles and the position of a colour stop. Without registration, a custom property is just a string, and a string can only jump from one value to the next.

## Why gradients will not transition on their own

Try `transition: background 0.4s` on a button whose hover state swaps one `linear-gradient()` for another and nothing eases: the new gradient snaps in. Gradients are images, and the specification treats `background-image` as not interpolable, so the browser has no way to blend one image into a different one.

The usual workaround is to oversize the background and slide `background-position`, which is how most [animated CSS gradients](https://gradiently.design/guide/css-animated-gradient) are built. It works, but it only moves a fixed gradient around. You cannot change a colour, rotate an angle or push a stop along. Registered properties change that, because the thing being animated is no longer the image but a typed number or colour inside it. The browser recalculates the gradient on every frame from the current value.

### Unregistered --tint

- Stored as a string of tokens
- Transitions jump halfway through
- Inherits by default
- An invalid value breaks the declaration at computed time

### Registered with @property

- Parsed as a real colour
- Transitions interpolate smoothly
- Inheritance is your choice
- An invalid value falls back to the initial value

## The @property syntax

An `@property` rule has three descriptors. `syntax` says what type of value is allowed, `inherits` says whether children receive the value, and `initial-value` is used when nothing else sets it. All three are required, except that `initial-value` may be left out when the syntax is `*`. If any required part is missing or wrong, the browser ignores the whole rule, silently.

```css
@property --tint {
  syntax: '<color>';
  inherits: false;
  initial-value: #7c3aed;
}

@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --stop {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 40%;
}
```

Three registered properties: a colour, an angle and a stop position. Register them once, at the top level of a stylesheet.

The initial value must be computationally independent, which in practice means absolute units: `0deg`, `40%`, `12px` and hex colours are fine, while `2em` or `var(--x)` are not. Setting `inherits: false` is usually what you want for animation, and it spares the browser from pushing the value down the tree.

| Syntax | Animates smoothly | Good for |
| --- | --- | --- |
| `<color>` | Yes | Gradient colours, theme shifts |
| `<angle>` | Yes | Linear angles, conic rotation |
| `<percentage>` | Yes | Colour stop positions, sizes |
| `<length>` | Yes | Radial sizes, offsets |
| `<number>` | Yes | Opacity style values, multipliers |
| `<integer>` | Yes, in whole steps | Counters, stepped effects |
| `*` | No, it flips | Anything you never animate |

The syntaxes that matter for gradient work. You can also accept several, such as `'<length> | <percentage>'`, or a list with `+`.

## Animate a gradient colour on hover

This is the smallest useful example. The button uses `--tint` as its second colour, and the hover state only changes `--tint`. Because the property is registered as a colour, the transition eases between violet and coral instead of snapping.

```css
@property --tint {
  syntax: '<color>';
  inherits: false;
  initial-value: #7c3aed;
}

.button {
  background: linear-gradient(120deg, #1e1b4b 0%, var(--tint) 100%);
  transition: --tint 400ms ease;
}

.button:hover {
  --tint: #fb7185;
}
```

The transition names the custom property itself. Writing transition: background would do nothing here.

- Resting: --tint is violet: `linear-gradient(120deg, #1e1b4b 0%, #7c3aed 100%)`
- Halfway through the transition: `linear-gradient(120deg, #1e1b4b 0%, #bb56b9 100%)`
- Hover: --tint is coral: `linear-gradient(120deg, #1e1b4b 0%, #fb7185 100%)`

Three frames of the same button. The middle colour only exists because the browser can interpolate a registered colour.

For more ways to treat buttons, see [gradient button CSS](https://gradiently.design/guide/css-gradient-button) and [CSS gradient hover effects](https://gradiently.design/guide/css-hover-gradient).

## Rotate a conic gradient border

A favourite use of `@property` is a border that seems to travel around a card. A [conic gradient](https://gradiently.design/guide/css-conic-gradient) starts from an angle, so registering that angle and animating it from `0deg` to `360deg` spins the colours. Two backgrounds do the work: the card colour clipped to the padding box, and the conic gradient clipped to the border box.

```css
@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.card {
  border: 2px solid transparent;
  border-radius: 16px;
  background:
    linear-gradient(#0f0b1e, #0f0b1e) padding-box,
    conic-gradient(from var(--angle), #22d3ee, #7c3aed, #f472b6, #22d3ee) border-box;
  animation: spin 6s linear infinite;
}

@keyframes spin {
  to { --angle: 360deg; }
}

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

Repeat the first colour at the end so the seam where 360deg meets 0deg disappears.

The conic ring at 0deg: `conic-gradient(from 0deg, #22d3ee, #7c3aed, #f472b6, #22d3ee)`

The same gradient the border uses. As --angle grows, this ring turns, and the thin strip you see around the card travels with it. [Gradient borders in CSS](https://gradiently.design/guide/css-gradient-border) covers the static version.

## Move a colour stop

A registered percentage lets a stop slide. Use it for a progress fill, a highlight that sweeps across a heading, or a horizon that rises on scroll. Here `--stop` moves the point where the gradient turns from ink to teal.

```css
@property --stop {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 20%;
}

.meter {
  background: linear-gradient(90deg, #0d9488 0%, #0d9488 var(--stop), #0f172a var(--stop));
  transition: --stop 600ms ease-out;
}

.meter[data-done] {
  --stop: 100%;
}
```

Two stops at the same position make a hard edge, and the edge moves as --stop changes.

## Registering from JavaScript

`CSS.registerProperty()` does the same job from a script. It is handy when a design system registers its properties at runtime, or when a value only becomes known after load. Registering the same name twice throws an error, so guard it.

```js
if ('registerProperty' in CSS) {
  try {
    CSS.registerProperty({
      name: '--angle',
      syntax: '<angle>',
      inherits: false,
      initialValue: '0deg',
    })
  } catch {
    // Already registered, which is fine.
  }
}
```

The JavaScript form uses initialValue in camel case. Everything else matches the CSS rule.

## Browser support, fallbacks and performance

`@property` works in current Chrome, Edge, Safari and Firefox; Firefox was the last to add it, in version 128. Check [caniuse](https://caniuse.com/mdn-css_at-rules_property) if you support older devices. A browser that does not understand the rule treats `--angle` as an ordinary custom property, so your gradient still draws at its starting value and simply does not move. That is a fine fallback, provided the static frame looks finished.

> **Mind the repaint** Animating a property used in `background` makes the browser repaint that element on every frame. A small border or button is cheap; a full screen hero animated forever costs battery. [CSS gradient performance](https://gradiently.design/guide/css-gradient-performance) has the measurements to watch.

1. **Register before you use** Put every `@property` rule at the top level, outside media queries, so it applies everywhere.
2. **Use typed syntaxes** Choose `<color>`, `<angle>` or `<percentage>`. Avoid `*` for anything you plan to animate.
3. **Transition the property** Write `transition: --tint 400ms`, naming the custom property rather than `background`.
4. **Respect reduced motion** Stop looping animations under `prefers-reduced-motion: reduce`, as described in [prefers reduced motion](https://gradiently.design/guide/reduced-motion).
5. **Check the still frame** Turn the animation off and make sure the resting gradient still looks intentional.

If the motion you want lives on social posts or video rather than a web page, a Gradiently Mark already carries its own light and slow motion, and Pro exports it as MP4, WebM or GIF where the browser supports recording, without writing keyframes. For everything on the web, `@property` is the cleanest tool you have. The [MDN reference](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) lists every descriptor.

## FAQ

### What does CSS @property do?

It registers a custom property with a type, an inheritance rule and an initial value. Knowing the type lets the browser validate the value and interpolate it in transitions and animations.

### Can you animate a CSS gradient?

Not directly, because background-image is not interpolable. Register the colours, angle or stop positions with @property, use them inside the gradient, and animate those properties instead.

### Why is my @property animation not working?

Usually a descriptor is missing, the initial value uses a relative unit, the syntax is `*`, or the transition names `background` instead of the custom property. Any invalid descriptor makes the browser ignore the whole rule.

### Does Firefox support @property?

Yes. Firefox added @property in version 128, so it now works in every major current browser.

### Should inherits be true or false?

Use false for most animated values. It keeps the value on the element where you set it and avoids extra work for the browser.
