Themeable effects with CSS custom properties
Custom properties store values in the cascade. Unlike preprocessor variables, they survive into the browser, inherit through the document, respond to media queries, and can be changed for one component instance without regenerating a stylesheet.
A good custom property is a small public API: its name explains intent, its scope matches the component that owns it, and its fallback keeps the declaration valid when no override exists.
Use the cascade as the configuration system
Declare broad design tokens on :root only when they truly belong to the entire application. Put component-specific controls on the component selector so names do not become accidental global contracts.
Custom property names are case-sensitive. --accent and --Accent are different values. A reference can include a fallback, and that fallback can itself contain another var() call.
- Design token
- A shared decision such as a color, spacing step, radius, or motion duration.
- Component property
- A local override point that belongs to one component contract.
- Fallback
- The second argument of
var(), used when the referenced property is missing or invalid.
:root {
--color-accent: #60a5fa;
--radius-control: .75rem;
}
.button {
--button-bg: var(--color-accent);
--button-fg: #08111f;
color: var(--button-fg, white);
background: var(--button-bg, #2563eb);
border-radius: var(--radius-control);
}
.button--danger { --button-bg: #fb7185; }Scope overrides near the element that owns them
Inheritance makes theme wrappers powerful: set a token on a subtree and every descendant that references it adapts. The same behavior can also create surprises when generic names leak too far.
Use names such as --card-border rather than --border for component contracts. Reserve short, broad names for intentionally global systems.
Register values that need interpolation
Unregistered custom properties are token streams, so browsers switch many of them discretely during animation. @property supplies a syntax, inheritance rule, and initial value, allowing numeric colors, lengths, angles, and percentages to interpolate.
Registration is global by name. Choose stable prefixed names in reusable components and make the initial value valid for the declared syntax.
@property --progress {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.meter {
background: conic-gradient(#38bdf8 var(--progress), #1e293b 0);
transition: --progress 500ms ease;
}
.meter.is-complete { --progress: 100%; }Update values without coupling to presentation
JavaScript can call element.style.setProperty('--progress', '72%') while CSS remains responsible for color, easing, and layout. This keeps state transfer small and avoids constructing whole style strings in application code.
Validate values before writing them and preserve semantic state in attributes or application data. A custom property is presentation input, not the only record of business state.
Live catalog examples
These production examples use --custom-property. Open any effect to inspect its complete source and experiment in the browser editor.
Browser support
Support notes refer to stable, unprefixed behavior unless a prefix is explicitly listed.
| Feature | Browsers | Note |
|---|---|---|
| Custom properties / var() | All modern browsers | Supported across the modern baseline. |
| @property | Modern Chromium, Firefox, Safari | Provide a static result when interpolation is optional. |
| CSS.registerProperty() | Modern browsers with support | The stylesheet @property form is usually easier to audit. |
Common pitfalls
Names differ only by letter case
Custom property names are case-sensitive; use one consistent naming convention.
An unregistered value is expected to interpolate
Register its syntax with
@propertywhen smooth numeric animation is required.No fallback is supplied
Add a meaningful second argument to
var()when the property may be absent.Every component value is placed on :root
Scope local contracts to their component and keep only true design tokens global.
Frequently asked questions
- Are custom properties the same as Sass variables?
- No. They participate in the runtime cascade, inherit, and can change in the browser after CSS is generated.
- Can JavaScript update them?
- Yes. Use style.setProperty on an element or a suitable ancestor.
- Why is my variable fallback not used?
- Fallbacks apply when the custom property is missing or invalid at substitution time, not merely because its value looks inconvenient.
- When should I use @property?
- Use it when you need typed validation, controlled inheritance, an initial value, or smooth interpolation.
Related guides
- conic-gradient()Rings, charts, and highlights with conic-gradientPaint angular color stops for progress rings, segmented charts, rotating glows, and compact procedural patterns.
- @keyframesCSS animations with @keyframesBuild reliable CSS animation timelines, combine multiple motions, and keep them smooth and accessible.
- :hoverInteraction effects with :hover and transitionsDesign responsive hover feedback that also works for keyboards, touch input, and reduced-motion users.
- perspective / transformPerspective and CSS 3D transformsConstruct stable 3D scenes, preserve depth across children, and avoid flattening, clipping, and unreadable text.
Last updated:
All guides