Articles

CSS @property: Typed Custom Properties Explained

@property registers a CSS custom property with a type, initial value, and inheritance rule — unlocking smooth animation and real error checking.

Takina Takina · · 4 min read
A close-up of CSS markup on a dark editor background

@property is a CSS at-rule that lets you register a custom property with an explicit syntax type, an initial value, and an inheritance rule — turning a plain, untyped --variable into something the browser actually understands and can animate. Without it, custom properties are just strings: the browser has no idea --angle is supposed to be an angle or --brand-color is supposed to be a color, so it can’t interpolate between two values during a transition.

The problem with plain custom properties

A standard custom property is defined with nothing more than a name and a value:

:root {
  --angle: 0deg;
}

To the CSS engine, --angle is just a token stream — it doesn’t know 0deg is an angle rather than an arbitrary string. That has a concrete consequence: if you try to transition --angle from 0deg to 360deg to spin a conic gradient, nothing animates. The browser can’t interpolate between two values it doesn’t recognize as numbers, so it just swaps the value at the halfway point of the transition, producing a jump instead of a spin.

Registering a typed property

@property fixes this by declaring the property’s type up front:

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

.dial {
  background: conic-gradient(from var(--angle), tomato, gold);
  transition: --angle 0.6s ease;
}

.dial:hover {
  --angle: 360deg;
}

Three fields are required:

  • syntax — the value type, such as "<color>", "<length>", "<number>", "<percentage>", or "<angle>". This is what enables interpolation.
  • inherits — whether elements inherit the property from their parent, exactly like the inherits behavior built into standard CSS properties.
  • initial-value — the fallback used if no value is set, similar to how color defaults to canvastext.

Once registered, the browser type-checks every assignment. A value that doesn’t match the declared syntax is rejected and falls back to the initial value instead of silently propagating a broken string through your gradients.

What this unlocks

  • Animatable gradients and angles. Rotating conic gradients, sweeping color stops, and other effects that were previously JavaScript-only become pure CSS.
  • Type safety. An invalid assignment — say, a plain string where a <color> is expected — is caught and discarded instead of quietly breaking downstream calculations.
  • Predictable inheritance. inherits: false stops a property from leaking into child elements the way an ordinary custom property would, which is useful for component-scoped tokens.

@property vs a plain custom property

Plain --custom-property@property-registered
Type checkingNone — any token stream is validEnforced against syntax
AnimatableOnly via discrete jumpsSmoothly interpolated if the syntax allows it
InheritanceAlways inheritsConfigurable via inherits
Initial valueEmpty/invalid by defaultExplicit initial-value
Invalid valuesBreak silentlyRejected, fall back to initial value

Falling back gracefully

Because @property is a progressive enhancement, it’s worth checking support with @supports before relying on it for anything critical:

@supports (background: paint(something)) or (color: color-mix(in srgb, red, blue)) {
  @property --brand-hue {
    syntax: "<number>";
    inherits: false;
    initial-value: 220;
  }
}

In practice, browsers that don’t understand @property simply ignore the at-rule and treat the referenced custom property as a plain, untyped one — the gradient or color still renders, it just won’t animate smoothly. That makes @property a safe addition to an existing stylesheet: worst case, an unsupported browser falls back to the same jump-cut behavior it already had.

A second example: typed color transitions

Typed properties aren’t limited to angles. A registered <color> property lets the browser interpolate between two colors properly, in the color space it understands, rather than treating the value as an opaque string:

@property --accent {
  syntax: "<color>";
  inherits: true;
  initial-value: #6366f1;
}

.button {
  background: var(--accent);
  transition: --accent 0.3s ease;
}

.button:hover {
  --accent: #ec4899;
}

Without registering --accent, hovering would swap the background color instantly at the midpoint of the transition instead of fading between the two.

Where it fits with modern CSS

@property pairs naturally with other CSS features that lean on custom properties as design tokens, like CSS cascade layers for organizing where those tokens are defined, or clamp()-based fluid typography where a typed <length> property can be animated on interaction. It’s also a natural companion to the View Transitions API, since transitions there rely on the same interpolation machinery — a typed custom property can drive a smooth cross-page animation the same way it drives a hover effect. And because @property behaves like a real CSS feature rather than a build-time convenience, it composes cleanly with container queries: a component can expose a typed property that responds to its container’s size without any JavaScript wiring it up.

Syntax strings beyond the basics

The syntax descriptor accepts more than single types. A pipe-separated list restricts the property to one of several alternatives, and a bare * accepts any value — effectively opting back into the untyped behavior of a plain custom property while still getting to set inherits and initial-value explicitly:

@property --gap-size {
  syntax: "<length> | <percentage>";
  inherits: false;
  initial-value: 1rem;
}

This is useful for properties that legitimately need to accept more than one unit type — a gap that’s sometimes a fixed length and sometimes a percentage of its container, for instance — without giving up type checking entirely for values outside that set.

The takeaway

@property upgrades a custom property from an untyped string to a real, type-checked CSS value with a defined initial value and inheritance behavior. The immediate payoff is animation — gradients, angles, and other values that used to require JavaScript can now transition natively — but the type checking alone is worth it for catching invalid values before they silently break your layout.

Takina Takina · · 4 min read

CSS object-fit and object-position, Explained

object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.

#CSS #Web Development #Frontend
Takina Takina · · 5 min read

CSS inherit, initial, unset & revert Explained

CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.

#CSS #Web Development #Frontend
Takina Takina · · 4 min read

Dynamic Viewport Units: dvh, svh, and lvh Explained

dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.

#CSS #Web Development #Frontend