CSS @property and Custom Property Registration

CSS @property lets you register a custom property so the browser knows its syntax, whether it inherits, and what its initial value should be. That makes CSS variables more predictable, easier to animate, and safer to use in complex styles.

Quick answer: Use @property when you want a CSS custom property with a defined type such as a number, color, or length. Registration lets the browser validate values early, provide a fallback initial value, and interpolate the property in animations.

Difficulty: Intermediate

You'll understand this better if you know: basic CSS custom properties with var(), how inheritance works in CSS, and the difference between a property name and a property value.

1. What Is @property?

@property is a CSS at-rule from the Properties and Values API. It registers a custom property so the browser can treat it like a real typed CSS property instead of an unstructured string.

Without registration, custom properties are still useful, but the browser treats them as token streams. With registration, the browser can validate and animate them as typed values.

2. Why @property Matters

Plain CSS custom properties are flexible, but that flexibility has limits. If a browser does not know a custom property's type, it cannot smoothly interpolate it in many animations, and it cannot always detect invalid values in a helpful way.

Registration matters when you want:

It is especially useful in design systems, animated UI themes, gradients, transforms, and components that expose CSS knobs to other developers.

3. Basic Syntax or Core Idea

The basic structure of @property is small, but each part matters. The browser uses these declarations to understand how to store and interpret the custom property.

Minimal registration

Here is a simple example that registers a numeric custom property:

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

This registration tells the browser that --progress must be a number, does not inherit from parents, and defaults to 0 if no value is set.

Using the registered property

After registration, you can use the custom property in normal CSS:

.meter {
  --progress: 0.75;
  transform: scaleX(var(--progress));
}

Because the property is typed as a number, the browser can interpret it as a number instead of a raw string.

4. Step-by-Step Examples

Example 1: A typed color token

A common use case is registering a color token for theming. This lets the browser animate between colors more smoothly.

@property --brand-color {
  syntax: "<color>";
  inherits: true;
  initial-value: #3b82f6;
}

.button {
  background-color: var(--brand-color);
}

This is useful when a design token should accept only valid colors and should pass through a theme hierarchy.

Example 2: Animating a length value

Registered length values can animate more naturally than untyped custom properties. The browser knows how to interpolate the value over time.

@property --card-offset {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.card {
  --card-offset: 24px;
  transform: translateY(var(--card-offset));
  transition: --card-offset 300ms ease;
}

Here, the browser can animate --card-offset as a real length value instead of a text string.

Example 3: A non-inheriting property for local state

Sometimes you want a custom property to affect only one component and not leak through descendants.

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

.dial {
  --rotation: 45deg;
  transform: rotate(var(--rotation));
}

This keeps the value scoped to the element unless you explicitly set it elsewhere.

Example 4: Fallback behavior through initial value

If a custom property is not set, the browser can use the registered initial value without needing an explicit fallback in every var() call.

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

.stack {
  display: grid;
  gap: var(--gap);
}

If --gap is not set, the layout still gets a valid gap value from the registration.

5. Practical Use Cases

@property is most useful when a custom property is more than a simple string token. Typical cases include:

It is less useful for arbitrary text values, selector names, asset URLs, or content fragments that do not benefit from typing.

6. Common Mistakes

Mistake 1: Forgetting that the syntax must match the value

Once a custom property is registered, the browser validates values against the declared syntax. If the value does not match, it is rejected.

Problem: This code assigns a value that does not match the registered type, so the browser ignores it and keeps the initial value instead.

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

.meter {
  --progress: 75%;
}

Fix: Use a value that matches <number>, or change the registration to <percentage> if that is what you need.

@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.meter {
  --progress: 75%;
}

The corrected version works because the registered syntax and assigned value now agree.

Mistake 2: Using an invalid initial value

The initial-value must also match the declared syntax. This catches many beginners because the error is in the registration, not the later usage.

Problem: The initial value does not satisfy the declared type, so the registration is invalid.

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

Fix: Provide a valid length such as 0px, 1rem, or another unit-based length.

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

The fixed version is valid because the initial value belongs to the declared value type.

Mistake 3: Expecting every custom property to animate smoothly

Unregistered custom properties often do not interpolate the way you expect. They can jump from one value to another instead of animating through intermediate states.

Problem: A plain custom property is treated like an untyped string, so the browser may not animate it as a numeric or color value.

.box {
  --x: 0px;
  transform: translateX(var(--x));
  transition: --x 300ms ease;
}

Fix: Register the property with the correct syntax so the browser can interpolate it as a typed value.

@property --x {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.box {
  --x: 0px;
  transform: translateX(var(--x));
  transition: --x 300ms ease;
}

The registered version works because the browser can now animate a known length value.

7. Best Practices

Practice 1: Register only properties that need typing

Not every CSS variable needs registration. Use @property where typing, inheritance control, or animation is valuable.

@property --surface-opacity {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

Typed registration is best when the property participates in math, animation, or strict theming.

Practice 2: Match the syntax as narrowly as possible

Use the narrowest value type that still fits your use case. A specific syntax helps the browser validate inputs and prevents accidental misuse.

@property --shadow-blur {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

A narrow syntax makes your API clearer to anyone consuming the stylesheet.

Practice 3: Decide inheritance intentionally

Think about whether the property should flow to descendants. Many component-level values should not inherit unless that behavior is part of the design.

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

Choosing inheritance on purpose prevents unexpected styling in nested components.

8. Limitations and Edge Cases

A common “not working” report is that the property still appears to inherit or still does not animate. In many cases the cause is either a browser that lacks support or a value that does not match the registered syntax.

9. Practical Mini Project

Let’s build a tiny themeable progress bar that uses typed custom properties for width and color. This shows why @property is useful in real component styling.

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

@property --bar-color {
  syntax: "<color>";
  inherits: true;
  initial-value: #22c55e;
}

.progress {
  --progress: 0.6;
  --bar-color: #3b82f6;
  width: 20rem;
  padding: 0.5rem;
  border: 1px solid #d1d5db;
  border-radius: 9999px;
}

.progress__bar {
  height: 0.75rem;
  width: calc(var(--progress) * 100%);
  background-color: var(--bar-color);
  border-radius: 9999px;
  transition: width 300ms ease, background-color 300ms ease;
}

This example uses --progress as a numeric multiplier and --bar-color as a typed theme color. The registration makes both values safer and more predictable.

10. Key Points

11. Practice Exercise

Expected output: A panel that fades between transparent and opaque states using a registered custom property.

Hint: Register the property as <number>, then apply it with the opacity property.

@property --panel-opacity {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

.panel {
  --panel-opacity: 0.5;
  opacity: var(--panel-opacity);
  transition: opacity 200ms ease;
}

The solution works because the custom property is registered as a number, then used directly by opacity, which also expects a numeric value in the same range.

12. Final Summary

@property turns a custom CSS variable into a registered, typed, and optionally animatable property. That gives you stricter validation, cleaner defaults, and more reliable behavior than an unregistered custom property.

It is especially valuable for design tokens and component state values such as colors, lengths, angles, and numbers. When you know the value shape ahead of time, registration helps the browser help you.

If you are already comfortable with custom properties and var(), the next step is to experiment with typed animation values like lengths and colors. That is where @property becomes most rewarding.