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.
- It defines the property name, usually starting with --.
- It declares the allowed syntax for values, such as <color> or <length>.
- It controls whether the property inherits.
- It sets an initial-value when nothing else is specified.
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:
- Cleaner animation of colors, numbers, lengths, angles, or percentages.
- More predictable inheritance behavior.
- Better defaults through initial-value.
- Stronger validation of values instead of silently accepting unusable strings.
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:
- Animating theme colors, tints, and gradients.
- Controlling component sizes with typed length values.
- Driving transforms such as translation, rotation, or scaling.
- Managing UI progress indicators and circular loaders.
- Building reusable design tokens with clear defaults.
- Creating local component state in pure CSS without script.
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
- @property only works for custom properties, not standard CSS properties.
- The browser must support the Properties and Values API; older browsers may ignore the registration and fall back to normal custom property behavior.
- Registered syntax is strict. A value that looks close but does not match the grammar is invalid.
- Some value types are more useful than others. For example, <length>, <color>, <number>, and <angle> are common, while highly complex syntax is harder to design well.
- If a property is unregistered, animation and validation behavior may differ across browsers and scenarios.
- Registration does not make a custom property magically affect layout unless you actually use it in another property such as width, transform, or color.
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
- @property registers a CSS custom property so the browser can type-check it.
- The three required pieces are syntax, inherits, and initial-value.
- Typed custom properties can animate more smoothly than unregistered ones.
- Use the narrowest syntax that fits the design token you are creating.
- Choose inheritance carefully, because it changes how values flow through descendants.
- Validation applies to both later assignments and the registered initial value.
11. Practice Exercise
- Register a custom property called --panel-opacity that accepts a number between 0 and 1 as a CSS number.
- Set the default to 1 and prevent inheritance.
- Use the property on a panel element to control its opacity.
- Add a transition so the opacity change animates smoothly when the custom property changes.
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.