The Complete Guide to CSS @property — From Design Token Type Registration to Style Dictionary v4 & Tailwind v4 Transition Automation
Have you ever experienced colors snapping abruptly during theme switches, or transitions failing to work on gradients when using CSS custom properties (--var)? The cause is simple: browsers store regular CSS variables as plain strings. Whether it's #6366f1 or 42px, from the browser's perspective it's just meaningless text — making numeric interpolation between two values impossible.
The @property at-rule solves this problem at the root. By declaring a type, initial value, and inheritance behavior on a custom property, the browser understands the meaning of the value, allowing transition and @keyframes to work correctly. On July 9, 2024, the last barrier to production use was removed when Chrome, Edge, Firefox, and Safari all completed support.
In this article, we'll walk through @property's core mechanics step by step — from how it works, to automatically generating declarations with Style Dictionary v4, to integrating them into a real-world pipeline with Tailwind CSS v4. Style Dictionary is a build system that transforms design token JSON into platform-specific code, and Tailwind v4 is a utility framework that uses CSS files themselves as the configuration source. If you're new to either tool, each section provides the necessary context, so feel free to follow along in order.
Core Concepts
CSS @property — Typed Custom Properties
@property (CSS Properties and Values API Level 1) is an at-rule that lets you explicitly register three pieces of information on a CSS custom property.
@property --color-primary {
syntax: "<color>";
inherits: true;
initial-value: #6366f1;
}
@property --spacing-base {
syntax: "<length>";
inherits: true;
initial-value: 16px;
}
@property --opacity-overlay {
syntax: "<number>";
inherits: false;
initial-value: 0;
}The three required descriptors each serve the following roles:
| Descriptor | Role |
|---|---|
syntax |
Allowed type (<color>, <length>, <number>, <angle>, <time>, *, etc.) |
inherits |
Whether to inherit the value from the parent (true / false) |
initial-value |
The default value matching the type (required when syntax is not *) |
Why can't regular CSS variables transition? The browser stores
--color: #fffas the string"#fff".transitionrequires numeric interpolation, but calculating between a start and end value is impossible with strings. The moment you registersyntax: "<color>"with@property, the browser recognizes this value as a color and can interpolate the R, G, and B channels numerically.
Why It Connects to Design Tokens
Design tokens are design decisions — colors, spacing, typography — abstracted into platform-independent values. When outputting these tokens as CSS custom properties, adding @property registration alongside them provides two benefits:
- Type safety: If an invalid value is provided, the browser silently falls back to
initial-value, preventing silent bugs. - Animation unlocked: During theme transitions, colors, spacing, angles, and more are smoothly interpolated with CSS alone.
Browser Support Status
On July 9, 2024, @property entered Baseline Newly Available status. It can now be used with confidence across all major browsers.
| Browser | Supported Version |
|---|---|
| Chrome / Edge | 85+ |
| Firefox | 128+ |
| Safari | 16.4+ |
Practical Application
Light/Dark Theme Color Transitions
This is the use case where you'll see the most immediate effect. Registering color tokens with @property means colors smoothly interpolate during theme switches instead of snapping abruptly.
/* globals.css */
/* 1. Register color token types */
@property --color-bg {
syntax: "<color>";
inherits: true;
initial-value: #ffffff;
}
@property --color-text {
syntax: "<color>";
inherits: true;
initial-value: #111111;
}
/* 2. Declare transitions on the root */
:root {
--color-bg: #ffffff;
--color-text: #111111;
transition: --color-bg 0.3s ease, --color-text 0.3s ease;
}
/* 3. Override only the dark theme values */
[data-theme="dark"] {
--color-bg: #0f0f0f;
--color-text: #f5f5f5;
}
body {
background-color: var(--color-bg);
color: var(--color-text);
}| Point | Description |
|---|---|
inherits: true |
All child elements inherit the value declared on the root |
Declaring transition on :root |
Toggling the data-theme attribute alone interpolates the entire page's colors |
| No JS required | No additional scripts beyond toggling the attribute |
Gradient Transitions
Previously, transitioning gradients with CSS alone was impossible. By registering each color stop as a <color> type, you can implement smooth gradient changes with a single transition line.
@property --grad-start {
syntax: "<color>";
inherits: false;
initial-value: #6366f1;
}
@property --grad-end {
syntax: "<color>";
inherits: false;
initial-value: #ec4899;
}
.button {
background: linear-gradient(135deg, var(--grad-start), var(--grad-end));
transition: --grad-start 0.4s ease, --grad-end 0.4s ease;
}
.button:hover {
--grad-start: #8b5cf6;
--grad-end: #f43f5e;
}Why use
inherits: false: The gradient colors are values controlled only within this component, so they are isolated from parent inheritance. For theme colors that need to be shared globally,inherits: trueis appropriate.
Progress Indicator Animation with Number Tokens
Using the <number> type, numeric values like progress percentages can also be animated naturally with @keyframes.
@property --progress {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
.progress-ring {
/* stroke-dasharray: 100 sets the total circumference to 100 */
stroke-dasharray: 100;
--progress: 0;
stroke-dashoffset: calc(100 - var(--progress));
transition: --progress 1s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Simply changing the --progress value from JS applies a smooth animation */
/* element.style.setProperty('--progress', '75'); */Style Dictionary v4 — @property Auto-Generation Formatter
Style Dictionary is an open-source build tool that transforms design token JSON into code for each platform — CSS, Android, iOS, and more. In v4, the existing CommonJS synchronous approach (v3) was discarded in favor of a complete rewrite with an ESM-based asynchronous architecture. As a result, await can now be used in custom formatters as well.
First, the token JSON structure that the formatter will read:
{
"color": {
"primary": {
"$type": "color",
"$value": "#6366f1"
},
"secondary": {
"$type": "color",
"$value": "#ec4899"
}
},
"spacing": {
"base": {
"$type": "dimension",
"$value": "16px"
}
}
}A custom formatter that reads this JSON and automatically generates @property declarations:
// style-dictionary.config.mjs
import StyleDictionary from 'style-dictionary';
const TYPE_MAP = {
color: '<color>',
number: '<number>',
dimension: '<length>',
duration: '<time>',
};
StyleDictionary.registerFormat({
name: 'css/at-property',
format: async ({ dictionary }) => {
// After applying transformGroup: 'css', token.name is kebab-case
// To use as a CSS variable name, you must manually prepend --
const registrations = dictionary.allTokens
.filter(token => TYPE_MAP[token.$type])
.map(token => `@property --${token.name} {
syntax: "${TYPE_MAP[token.$type]}";
inherits: true;
initial-value: ${token.value};
}`)
.join('\n\n');
const variables = dictionary.allTokens
.map(token => ` --${token.name}: ${token.value};`)
.join('\n');
return `${registrations}\n\n:root {\n${variables}\n}`;
},
});
export default {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
destination: 'tokens.css',
format: 'css/at-property',
},
},
};| Code Point | Description |
|---|---|
--${token.name} |
token.name with transformGroup: 'css' applied is kebab-case like color-primary, so the -- prefix must be manually prepended to form a valid CSS variable name |
TYPE_MAP |
A mapping table that converts token $type to CSS syntax strings — types can be freely added as needed |
filter(token => TYPE_MAP[token.$type]) |
Only selects tokens with mappable types as @property registration targets |
format: async |
v4's asynchronous formatter signature, enabling external API calls that use await |
Tailwind CSS v4 — @theme and @property
Tailwind v4 has been completely redesigned, dropping the existing tailwind.config.js file in favor of using a CSS file itself as the single configuration source. Declaring tokens in an @theme block causes the engine to internally auto-generate @property declarations and dynamically produce utility classes like bg-brand-500 and duration-fast.
/* globals.css */
@import "tailwindcss";
@theme {
--color-brand-500: #6366f1;
--color-brand-600: #4f46e5;
--duration-fast: 150ms;
--duration-base: 300ms;
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--color-hero-from: #6366f1;
--color-hero-to: #ec4899;
}
/*
Tailwind's automatic @property registration is limited to Tailwind's internal tokens.
To use custom tokens added to @theme in transitions,
you must explicitly declare @property separately.
*/
@property --color-hero-from {
syntax: "<color>";
inherits: false;
initial-value: #6366f1;
}
@property --color-hero-to {
syntax: "<color>";
inherits: false;
initial-value: #ec4899;
}
.hero {
background: linear-gradient(135deg, var(--color-hero-from), var(--color-hero-to));
transition: --color-hero-from 0.5s ease, --color-hero-to 0.5s ease;
}Style Dictionary + Tailwind v4 Integration Pipeline
The complete workflow from Figma to final CSS:
Design tool (Figma / Tokens Studio)
↓ Export JSON tokens
Style Dictionary v4
├── Custom formatter: auto-generate @property declarations
└── Output: tokens.css
↓ Import CSS file
globals.css (merged into @theme block)
↓ Tailwind v4 engine processing (Lightning CSS)
Final CSS (type registration + utility classes)/* globals.css */
@import "tailwindcss";
@import "./tokens.css"; /* Style Dictionary output */
/*
@theme inline: instead of Tailwind re-outputting variables in the @theme block
as separate CSS variables, values are inlined directly at the reference site.
Since --color-brand-500 is already defined in tokens.css, this prevents duplicate output.
*/
@theme inline {
--color-brand: var(--color-brand-500);
--duration-transition: var(--duration-base);
}Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Type safety | When an invalid value is entered, the browser falls back to initial-value, preventing silent bugs |
| Animation unlocked | Values that previously couldn't be interpolated — gradients, angles, numbers — can now be controlled with transition/@keyframes |
| Performance improvement | Type information gives browsers more room for rendering optimization; Tailwind v4 leverages this to improve render speed on large pages |
| Pure CSS | Registration is complete with just a CSS file — no CSS.registerProperty() needed |
| Standards-based | Long-term stability guaranteed by W3C specification |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Browser support floor | Not supported in older browsers below Firefox 128+ and Safari 16.4+ | Apply Progressive Enhancement pattern |
initial-value required |
Registration fails entirely if omitted when syntax is not * |
Validate missing initial-value tokens at build time in the formatter |
| Fixed global scope | @property always registers at the global (:root) scope; component encapsulation is not possible |
Use regular --var without @property for component-local variables |
| Style Dictionary migration | Switching to v4 requires a complete rewrite: CJS → ESM, synchronous → asynchronous API | Refer to the official migration guide; incremental transition recommended |
Tailwind @theme limitation |
Auto-registration of @property is not supported for user-defined tokens |
Explicitly add separate @property declarations |
Progressive Enhancement:
@propertydeclarations are silently ignored by unsupported browsers. In environments without@propertysupport, CSS variables behave like regular variables — only transitions are lost, while basic functionality is preserved. To detect support from JavaScript, use the following:
// Detect @property support (JavaScript)
const supportsAtProperty = CSS.supports(
'@property --x { syntax: "<color>"; inherits: false; initial-value: red; }'
);
if (supportsAtProperty) {
// Provide enhanced transition experience
}/* CSS fallback pattern — browsers that don't support @property ignore the @property block */
:root {
--color-bg: #ffffff; /* Unsupported: value changes instantly (no transition) */
}
/* Supported: register @property and enable transition */
@property --color-bg {
syntax: "<color>";
inherits: true;
initial-value: #ffffff;
}
:root {
transition: --color-bg 0.3s ease;
}The Most Common Mistakes in Practice
-
Omitting
initial-value: Every type wheresyntaxis not*requiresinitial-value. Omitting it invalidates the@propertyregistration entirely, causing it to behave like a regular CSS variable. It's recommended to add validation logic to your Style Dictionary formatter to catch missinginitial-valueat build time. -
Misunderstanding
@propertydeclaration precedence: Per CSS cascade rules,@propertyalso follows the rule that later declarations override earlier ones, just like regular declarations. However, since an already-registered property is only updated when it follows the same specification, it's more predictable and maintainable to consolidate all@propertydeclarations for the same name in a single token file. -
Using Tailwind
@themetokens directly in transitions: User-defined tokens declared inside@themeare not subject to Tailwind's internal processing, so they are not automatically registered as@property. To apply transitions, you must explicitly declare@propertyseparately.
Closing Thoughts
Your design tokens are no longer mere CSS variables — they are type-safe contracts that the browser understands. @property is a standard that has been production-ready across all major browsers since July 2024, and it can scale to team-wide automation through the Style Dictionary v4 and Tailwind v4 pipeline.
Here are 3 steps you can start with right now:
-
Try registering
@propertyon a single color token in an existing project. Pick the most frequently used color variable — something like--color-primary— addsyntax: "<color>"andinitial-value, declaretransition: --color-primary 0.3s easeon:root, then change the value to see an immediate effect. -
Consider introducing a Style Dictionary v4 custom formatter to auto-generate
@propertyduring the token build step. Use thecss/at-propertyformatter example in this article as a starting point and extend theTYPE_MAP. When you have more than 100 tokens, adding validation logic for missinginitial-valueto the formatter lets you catch errors at build time before they cause problems. -
If you're using Tailwind v4, define your tokens in
@themeand explicitly add@propertytoglobals.cssfor any custom tokens that need transitions. Writing JS detection code and CSS fallback patterns alongside ensures users on older browsers still get the baseline experience. Automating the Style Dictionary build in your CI/CD pipeline completes a token sync workflow where Figma changes are immediately reflected in code.
Next article: We'll look at how to use the
transitiontype tokens newly added in the W3C DTCG 2025.10 draft — integrating them with Figma Tokens Studio and Style Dictionary to fully synchronize transition effects between design and code.
References
Good resources to read first as an introduction
- @property: Next-gen CSS variables now with universal browser support | web.dev
- @property | MDN Web Docs
- @property | CSS-Tricks Almanac
- CSS @property and the New Style | Ryan Mulligan
Practical application and in-depth resources
- The Times You Need A Custom @property Instead Of A CSS Variable | Smashing Magazine
- Using @property for loosely typed CSS custom properties | LogRocket Blog
- CSS @property Explained: The Secret Weapon for Dynamic & Smooth Animations | DEV Community
- Exploring Typesafe design tokens in Tailwind 4 | DEV Community
- BYO CSS tokens to Tailwind v4's new CSS-centric config | nerdy.dev
Official tool documentation