Combining @property Typed Custom Properties with Tailwind CSS
AI generated
tw
Tailwind CSS · CSS @property · Animation
@property for Typed Custom Properties
Animatable CSS variables with typed values, combined with Tailwind CSS

Classic CSS custom properties are plain strings with no type, which is why the browser can't animate them even when the value looks like a number or an angle. The @property rule lets you register custom properties with a genuine syntax type, an inheritance behavior, and an initial value instead, which suddenly makes them animatable. This article covers how to combine @property meaningfully with Tailwind CSS, including a practical example animating a gradient angle rotation.

14 min read @property · Typed variables Animatable gradient angles

1. Why classic custom properties aren't animatable

A classic CSS custom property, declared via --angle: 45deg, is internally nothing more than a string as far as the browser is concerned. The browser only interprets that value at the point where the property actually gets consumed via var(), for example in transform: rotate(var(--angle)). Up to that point of use, the browser has no idea whether it's an angle, a color, or any other CSS syntax, and that's exactly what makes animating this property impossible, since animation requires the browser to be able to meaningfully interpolate between two values.

Without a known type, the browser can't know, during a transition from --angle: 0deg to --angle: 360deg, that it should interpolate smoothly between the two angle values. Instead, with a classic custom property the value jumps abruptly from the old to the new value, with none of the intermediate steps that would make a visible animation. That's exactly the gap the @property rule closes, by assigning the custom property a genuine, browser-understood type.

2. The syntax of the @property rule in detail

An @property registration consists of three required fields: syntax defines the allowed value type, for example <angle> for angles, <color> for colors, or <length> for length values. inherits determines whether the property is inherited by child elements, and initial-value defines the value that applies wherever the property hasn't been explicitly set. All three fields are mandatory; an @property rule missing any of the three is discarded as invalid by the browser and ignored.

The registration itself sits as a standalone at-rule block in the stylesheet, independent of any particular selector, similar to @font-face or @keyframes. The property name in the @property rule and the name used later via var() must match exactly, including the two leading dashes, since CSS custom properties are always treated as case-sensitive.


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

.rotating-border {
  background: conic-gradient(from var(--gradient-angle), #6366f1, #ec4899, #6366f1);
}

3. Benefits of typing beyond pure interpolation

The most obvious benefit is animatability, but typing brings two more practical improvements along with it. First, the browser validates values against the declared syntax type: if an attempt is made to assign an invalid value like a string to a property typed as <angle>, the browser discards that value and falls back to the initial-value, instead of silently accepting the bad value as a string the way a classic custom property would.

Second, inherits: false can specifically prevent a property from getting unintentionally picked up by child elements, which occasionally causes surprising behavior with classic custom properties that always inherit, for example when a nested element suddenly shows the same gradient angle as its parent even though no value was ever set for it directly. For local, component-scoped values like an animation intermediate state, inherits: false is almost always the right call.

4. Animatable custom properties: a gradient angle example

A concrete example where a typed custom property makes the crucial difference is animating the angle in a conic-gradient(). Without @property, a rotating gradient border can only be built via a @keyframes rule that defines the entire background value with hardcoded intermediate angles, which is inflexible and combines poorly with dynamically computed start or end angles.

With a property typed as <angle>, a transition or @keyframes animation can instead simply target the custom property itself, and the browser automatically interpolates every intermediate angle between the start and target value. That reduces the animation to a single animated property, while the actual gradient declaration stays unchanged and merely refers via var() to the current, changing angle.


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

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

.rotating-border {
  background: conic-gradient(from var(--gradient-angle), #6366f1, #ec4899, #6366f1);
  animation: rotate-gradient 4s linear infinite;
}

5. Integration with Tailwind CSS v4

Tailwind CSS v4 relies on @property internally itself, among other things for some of its built-in gradient and transform utilities, to make their intermediate values animatable. For custom typed properties of your own, Tailwind v4's central CSS file with the @theme directive is a natural place, since it can hold arbitrary additional @property registrations alongside design tokens, keeping the typing in the same place as the rest of the Tailwind configuration.

Actually accessing the typed property within a utility context works via Tailwind's arbitrary-value syntax, for example bg-[conic-gradient(from_var(--gradient-angle),theme(colors.indigo.500),theme(colors.pink.500))], or more pragmatically through a dedicated component class that encapsulates the gradient declaration via @apply or plain CSS. For animations driven by Alpine.js or JavaScript, the current angle can additionally be set directly on the element via an inline style attribute with --gradient-angle: 120deg, with no Tailwind class needed for that at all.

6. The finer points of inherits and initial-value

The inherits flag has an effect that goes beyond plain inheritance: with inherits: false, the browser consistently resets the property to initial-value on every element that hasn't set an explicit value for it, regardless of whatever value applied on the parent element. With inherits: true, on the other hand, the parent element's value gets picked up, exactly as with a classic custom property. For animated states meant to stay independent per element, inherits: false is almost always the right choice.

initial-value must be a valid value for the declared syntax type, otherwise the entire @property rule gets discarded as invalid, not just the faulty field. An @property registration with syntax: "<angle>" and initial-value: 0 without a unit is therefore invalid, since an angle value always requires a unit such as deg, rad, or turn. This strict validation mechanism prevents silent errors, but a typo also produces no console error, the property just goes unused.

7. Browser support and fallback strategy

The @property rule is supported by all current versions of Chrome, Edge, and Safari, Firefox caught up with full support starting at version 128, so the feature is now considered production-ready for most projects. For projects that still need to support older Firefox versions, an unregistered custom property continues to behave like a classic, untyped property, so the layout doesn't break, only the animation of the property itself won't work.

A robust fallback strategy therefore combines a static, non-animated state as the base value with an animation gated behind @supports, activated only in browsers that support @property. That way the element still looks appropriate in older browsers, just without the rotation, instead of ending up in a broken or unexpected state. The check runs via @supports (background: paint(something)) or more directly via feature detection on @property itself, depending on the desired level of rigor.


.rotating-border {
  background: conic-gradient(from 0deg, #6366f1, #ec4899, #6366f1);
}

@supports (background: paint(something)) {
  @property --gradient-angle {
    syntax: "<angle>";
    inherits: false;
    initial-value: 0deg;
  }

  .rotating-border {
    background: conic-gradient(from var(--gradient-angle), #6366f1, #ec4899, #6366f1);
    animation: rotate-gradient 4s linear infinite;
  }
}

8. Practical example: an animated button border with Tailwind

A common use case is a call-to-action button with a gently rotating gradient border meant to draw attention on a primary button without feeling pushy. The trick is building the button background and the animated gradient border as two stacked layers: an outer layer with the animated conic-gradient() as its background, and a slightly smaller inner layer with the button's actual background, so only a thin strip of the outer gradient stays visible as a border.

Translated into Tailwind classes, that means a relatively positioned outer element carrying the typed custom property and the animation, plus an absolutely positioned inner element with inset-0.5 and the actual button background color, covering most of the outer surface. The text class and the click handler for the button sit on the inner element, so the animated border stays purely decorative and doesn't claim its own interaction surface.

9. Limits and common pitfalls

A common trap is picking a syntax type that's too broad, for example syntax: "*", which allows any arbitrary value but in doing so gives up exactly the type safety and animatability that justify using @property in the first place. An animatable property always needs a concrete type like <angle>, <length>, <color>, or <number>; a generic wildcard type behaves, as far as animation goes, exactly like a classic, untyped custom property.

A second pitfall concerns the registration itself within component-based build systems: if the same @property rule accidentally gets registered multiple times with differing syntax values across a project, for example because two independent components reuse the same property name, whichever definition loads last wins depending on the browser, which can cause inconsistent behavior between development and production environments. A single central location for all @property registrations, for example right next to the Tailwind @theme definition, reliably prevents this problem.

Field Required Example value Effect
syntax yes "<angle>" Defines the allowed value type and enables interpolation
inherits yes false Controls whether child elements pick up the value without their own declaration
initial-value yes 0deg Value that applies without an explicit assignment or on an invalid value
@supports fallback recommended @supports (background: paint(something)) Static state for browsers without @property support

Mironsoft

Tailwind CSS architecture, design systems, and performance

Tailwind frontends that stay maintainable despite thousands of utility classes?

We review existing Tailwind projects for bloated class lists, inconsistent design tokens, and unused CSS remnants, then build a design system that scales cleanly instead of getting messier with every component.

Design System Review

Checking tokens, spacing scale, and component consistency for maintainability.

Performance Optimization

Systematically reducing CSS bundle size, purge configuration, and load times.

Component Architecture

Building reusable, well-structured components instead of sprawling class lists.

10. Summary

@property with Tailwind: The Essentials at a Glance

Core idea

@property gives a custom property a genuine type, letting the browser interpolate between values and animate the property.

Required fields

syntax, inherits and initial-value must all three be set, otherwise the entire registration is discarded as invalid.

Practical example

An animated conic-gradient() angle for rotating button borders, not animatable with classic custom properties.

Fallback

A static, non-animated base state outside @supports, with the animation activated only in supporting browsers.

11. FAQ: @property with Tailwind: The Essentials at a Glance

1Why can't a classic CSS custom property be animated?
Because the browser treats it internally as a plain string with no type, and therefore has no way to know how to meaningfully interpolate between two values.
2Which fields are mandatory for @property?
syntax, inherits and initial-value. If any of the three is missing, the browser discards the entire registration as invalid.
3What does the syntax type in @property define?
It defines the allowed value type of the property, for example for angles or for colors, and is a prerequisite for animatability.
4What happens with inherits: false?
Child elements without their own declaration always get the initial-value, regardless of whatever value applied on the parent, instead of inheriting the value like a classic property.
5How do I animate a gradient angle with @property?
By typing the custom property as and then animating exactly that property via @keyframes or transition, while the gradient declaration itself stays unchanged.
6How do I combine @property with Tailwind CSS v4?
Via the central CSS file with the @theme directive for the registration, and via arbitrary-value syntax or dedicated component classes for access in markup.
7Which browsers support @property?
Chrome, Edge and Safari fully in current versions, Firefox since version 128. Older Firefox versions still support the property, just without animatability.
8How do I add a fallback for @property in older browsers?
With a static base value outside an @supports rule and the actual animation inside @supports (background: paint(something)) or a comparable feature detection.
9What happens with an invalid initial-value?
The entire @property rule gets discarded as invalid by the browser, not just the faulty field, with no error appearing in the console.
10Why should I avoid syntax: "*"?
Because a generic wildcard type gives up exactly the type safety and animatability that @property is meant to provide in the first place, behaving like a classic untyped property instead.