Home / Articles / Color & Theming /

colorcustom-properties

@property: typed custom properties

Register custom properties with a syntax type, initial value, and inheritance — unlocking smooth color animations and type-safe design tokens.

Why custom properties cannot animate by default

Standard CSS custom properties (declared with --name: value) are treated as untyped strings. The browser has no idea whether --accent holds a color, a length, or a random string. Because it cannot interpolate between two arbitrary strings, transitions and animations on custom properties simply snap from one value to another.

The @property at-rule solves this by registering a custom property with an explicit type. Once the browser knows a property holds a <color>, it can smoothly interpolate between two color values.

/* Without @property — the color snaps instantly */
.card {
  --bg: oklch(0.19 0.02 260);
  background: var(--bg);
  transition: --bg 0.3s;
}
.card:hover {
  --bg: oklch(0.25 0.04 265);
  /* Nothing animates — browser sees two strings */
}

/* With @property — smooth color transition */
@property --bg {
  syntax: "<color>";
  inherits: false;
  initial-value: oklch(0.19 0.02 260);
}
.card {
  background: var(--bg);
  transition: --bg 0.3s;
}
.card:hover {
  --bg: oklch(0.25 0.04 265);
  /* Smoothly animates through OKLCH color space */
}

The @property syntax

The @property rule has three required descriptors: syntax, inherits, and initial-value. All three must be present for the registration to be valid.

@property --gradient-angle {
  syntax: "<angle>";      /* what type of value this holds */
  inherits: false;          /* does it inherit down the DOM tree? */
  initial-value: 0deg;      /* default when no value is set */
}

/* Supported syntax types:
   "<color>"           — any CSS color value
   "<length>"          — px, rem, em, etc.
   "<percentage>"      — 0% to 100%
   "<number>"          — unitless number
   "<angle>"           — deg, rad, turn
   "<integer>"         — whole numbers only
   "<length-percentage>" — length or percentage
   "<custom-ident>"    — a CSS identifier
   "*"                  — any value (no type checking)
*/

Setting inherits: false means child elements do not receive the property from their parent. This is usually what you want for component-scoped values. Set it to true for theme-level tokens that should cascade.

Animating gradient hues

One of the most popular use cases for @property is animating values inside gradients. You cannot transition a background gradient directly, but you can transition a typed custom property that the gradient references.

@property --hue-start {
  syntax: "<number>";
  inherits: false;
  initial-value: 265;
}

@property --hue-end {
  syntax: "<number>";
  inherits: false;
  initial-value: 300;
}

.gradient-card {
  background: linear-gradient(
    135deg,
    oklch(0.5 0.2 var(--hue-start)),
    oklch(0.6 0.18 var(--hue-end))
  );
  transition: --hue-start 0.6s, --hue-end 0.6s;
}

.gradient-card:hover {
  --hue-start: 200;
  --hue-end: 170;
  /* The gradient hues shift smoothly from purple → teal */
}

This technique is also powerful for @keyframes animations. You can create endlessly cycling gradient effects by animating the hue property through 360 degrees.

Animating color stops in keyframes

Combining @property with @keyframes lets you create animated gradients that loop smoothly. Here is a background animation that cycles through the color wheel:

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

.animated-bg {
  background: linear-gradient(
    135deg,
    oklch(0.45 0.22 var(--gradient-hue)),
    oklch(0.55 0.18 calc(var(--gradient-hue) + 60deg))
  );
  animation: rotate-hue 8s linear infinite;
}

@keyframes rotate-hue {
  to {
    --gradient-hue: 360deg;
  }
}

/* The gradient smoothly cycles through every hue,
   maintaining consistent perceived brightness thanks to OKLCH */

Without @property, the --gradient-hue value would snap from 0 to 360 in a single frame. Type registration is what makes smooth interpolation possible.

Type-safe design tokens

Beyond animation, @property provides type safety for your design tokens. If a component expects a color and receives a length, the initial value kicks in instead of breaking the layout.

@property --surface-color {
  syntax: "<color>";
  inherits: true;
  initial-value: oklch(0.19 0.02 260);
}

@property --radius {
  syntax: "<length>";
  inherits: false;
  initial-value: 0.5rem;
}

.card {
  background: var(--surface-color);
  border-radius: var(--radius);
}

/* If someone accidentally writes: */
.broken {
  --surface-color: 42px;
  /* Browser ignores invalid value, uses initial-value instead.
     The card still gets oklch(0.19 0.02 260) — not a broken layout. */
}

This fallback behavior makes @property especially useful in design systems where custom properties are consumed by many different components. A typo or misconfiguration produces a graceful fallback rather than a visual bug.

Practical patterns

Here are patterns that combine @property with OKLCH for real-world UI components:

/* Pattern 1: Smooth theme switching */
@property --theme-bg {
  syntax: "<color>";
  inherits: true;
  initial-value: oklch(0.13 0.02 260);
}

@property --theme-text {
  syntax: "<color>";
  inherits: true;
  initial-value: oklch(0.93 0.01 260);
}

body {
  background: var(--theme-bg);
  color: var(--theme-text);
  transition: --theme-bg 0.5s ease, --theme-text 0.5s ease;
}

/* Pattern 2: Progress bar with animated color */
@property --progress-hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 25;
}

.progress-bar {
  background: oklch(0.6 0.2 var(--progress-hue));
  transition: --progress-hue 0.4s, width 0.4s;
}

/* Red (25) at 0%, yellow (85) at 50%, green (145) at 100% */
.progress-bar[data-value="50"] { --progress-hue: 85; }
.progress-bar[data-value="100"] { --progress-hue: 145; }

Browser support for @property is excellent in 2026: Chrome 85+, Safari 15.4+, Firefox 128+. It is safe to use without a fallback in most projects.