What Are CSS Custom Properties
Custom properties (sometimes referred to as CSS variables or cascading variables) are entities defined by CSS authors that represent specific values to be reused throughout a document. They are set using the @property at-rule or by custom property syntax (e.g., --primary-color: blue;). Custom properties are accessed using the CSS var() function (e.g., color: var(--primary-color);). This native browser capability eliminates the need for external tools or build processes that preprocessor variables require, making custom properties a truly portable and runtime-accessible feature of modern CSS.
The distinction between CSS custom properties and preprocessor variables is crucial for understanding their unique capabilities. Preprocessor variables are essentially find-and-replace operations that happen during compilation--once your CSS is generated, those variables no longer exist. CSS custom properties, conversely, exist in the browser's CSS Object Model (CSSOM) and can be read, written, and manipulated by JavaScript at any time. This runtime accessibility opens possibilities for dynamic theming, user preference detection, and responsive adjustments that would be impossible with preprocessor-only solutions. A website built with Next.js can respond to system theme preferences or user color choices instantly, without requiring a page reload or recompilation.
Complex websites have very large amounts of CSS, and this often results in a lot of repeated CSS values. For example, it's common to see the same color used in hundreds of different places in stylesheets. Changing a color that's been duplicated in many places requires a search and replace across all rules and CSS files. Custom properties allow a value to be defined in one place, then referenced in multiple other places so that it's easier to work with. Another benefit is readability and semantics--for example, --main-text-color is easier to understand than the hexadecimal color #00ff00, especially if the color is used in different contexts.
The Evolution of Variables in CSS
Before custom properties existed, developers relied on various workarounds to achieve similar results. Preprocessor variables from Sass, Less, and other CSS preprocessors provided a partial solution, but their compiled nature meant the variables disappeared in the final CSS output. This limitation became increasingly problematic as web applications demanded more dynamic styling capabilities. The CSS Working Group recognized the need for native, runtime-accessible variables and developed the custom properties specification as part of the CSS Custom Properties for Cascading Variables Module Level 1.
The specification introduced a novel approach that leveraged the cascade itself as a mechanism for variable management. Unlike preprocessor variables, which are essentially text substitution, CSS custom properties are actual CSS properties that participate in the cascade. This means they inherit from parent elements, can be scoped to specific selectors, and can be modified by any CSS rule that matches an element. The result is a flexible system that combines the convenience of variables with the full power of CSS's cascade and inheritance mechanisms.
1:root {2 --primary-color: #0066cc;3 --font-family-base: system-ui, sans-serif;4 --spacing-unit: 8px;5}6 7.element {8 background-color: var(--primary-color);9 padding: var(--spacing-unit);10}Declaring Custom Properties
Using the Two-Dash Prefix
In CSS, you can declare a custom property using two dashes as a prefix for the property name, or by using the @property at-rule. The two-dash prefix convention distinguishes custom properties from all other CSS properties, which use alphanumeric names. A custom property prefixed with two dashes begins with --, followed by the property name (e.g., --my-property), and a property value that can be any valid CSS value. Like any other property, this is written inside a ruleset, meaning you need a selector to define where the variable is available.
The selector given to the ruleset defines the scope in which the custom property can be used. For this reason, a common practice is to define custom properties on the :root pseudo-class, so that it can be referenced globally throughout the document. However, this doesn't always have to be the case--you may have good reasons for limiting the scope of your custom properties to specific components or sections of your site.
Custom property names are case sensitive----my-color will be treated as a separate custom property to --My-color. This case sensitivity follows the same rules as other CSS properties and requires careful attention when naming variables. Many teams establish naming conventions to prevent confusion, such as using lowercase exclusively or following a specific prefix pattern like --color- or --spacing- for related variables.
Advanced Declaration with @property
The @property at-rule allows you to be more expressive with the definition of a custom property, with the ability to associate a type with the property, set default values, and control inheritance. This provides additional validation and tooling support compared to standard property declarations. When you use @property, you specify three key components: the syntax (expected type of value), whether the property inherits from its parent, and the initial value.
The syntax property tells the browser what type of value the custom property should accept--whether it's a color, length, number, or other CSS value type. This enables better developer experience with tools that can provide autocomplete and type checking. The inherits property controls whether the custom property follows normal CSS inheritance rules or uses its initial value by default. For colors and other design tokens that you typically want to apply consistently, setting inherits to false can prevent unexpected cascading behavior.
If you want to define or work with custom properties in JavaScript instead of directly in CSS, there is a corresponding API for this purpose through the CSS Properties and Values API. This JavaScript API mirrors the @property at-rule, allowing programmatic creation of typed custom properties with the same benefits of type validation and inheritance control.
1@property --logo-color {2 syntax: "<color>";3 inherits: false;4 initial-value: #c0ffee;5}6 7/* JavaScript equivalent */8if ('registerProperty' in CSS) {9 CSS.registerProperty({10 name: '--my-color',11 syntax: '<color>',12 inherits: false,13 initialValue: '#ff6600'14 });15}Strategies for organizing custom properties across your codebase
Global Design Tokens
Define foundational values like colors, spacing, and typography scales on :root for universal access across your application.
Semantic Variables
Create purpose-based mappings from raw tokens to meaningful names like --color-primary or --spacing-container.
Component Scoping
Isolate component-specific variables within component selectors to prevent leakage and improve portability.
Cascade Override Pattern
Use cascade behavior to override variables in specific contexts for themes, responsive variants, or modes.
Using the var() Function
Regardless of which method you choose to define a custom property, you use them by referencing the property in a var() function in place of a standard property value. The var() function retrieves the value of the custom property at the point where it's used, meaning it reflects any current value from the cascade rather than a static value captured at definition time. This dynamic evaluation is what makes custom properties so powerful for responsive and theming scenarios.
Variables do not work inside media queries and container queries as property names or selectors. You can use the var() function in any part of a value in any property on an element, but you cannot use it for property names, selectors, or anything aside from property values. This means you can't write something like --{property-name}: value or use variables in @media rules directly--the variable must be part of an existing property's value.
Fallback Values for Robust Styles
The var() function accepts an optional second parameter--a fallback value that is used when the custom property is not defined or has an invalid value. Fallback values provide graceful degradation when custom properties might not be available, whether due to older browsers, component isolation, or other scoping issues. This makes your stylesheets more resilient and ensures components remain functional even without their expected custom property values.
Fallback values can even include other var() calls, creating chains of fallbacks that provide multiple levels of graceful degradation. This pattern is useful when progressively enhancing styles or supporting multiple design system versions simultaneously.
Computations with calc()
The calc() function works seamlessly with custom properties, enabling mathematical operations on variable values. This combination is particularly powerful for creating scalable design systems where values derive from base units. You might define a spacing scale using a base unit variable, then derive all spacing values through calculations rather than hard-coded numbers.
This approach ensures consistency across your entire design system and makes global adjustments trivial--changing the base spacing value updates all derived values automatically. For responsive design, you can combine calculations with viewport-relative units to create fluid typography and spacing systems that scale smoothly across device sizes.
1/* Fallback values */2.button {3 background-color: var(--button-bg-color, #0066cc);4 color: var(--button-text-color, white);5 padding: var(--button-padding, 0.75rem 1.5rem);6}7 8/* Chained fallbacks */9.element {10 background-color: var(--brand-primary, var(--fallback-blue, #2196f3));11}12 13/* Calculations with custom properties */14:root {15 --base-spacing: 8px;16 --space-xs: calc(var(--base-spacing) * 0.5);17 --space-sm: var(--base-spacing);18 --space-md: calc(var(--base-spacing) * 2);19 --space-lg: calc(var(--base-spacing) * 3);20 --space-xl: calc(var(--base-spacing) * 4);21}Cascade Behavior and Inheritance
Custom properties defined using two dashes are subject to the cascade and inherit their value from their parent. This inheritance behavior means that a custom property defined on a parent element is automatically available to all descendant elements, following standard CSS inheritance rules. However, you can override this behavior using the @property at-rule with inherits: false, which creates a custom property that always uses its initial value regardless of parent values.
For some CSS declarations, it is possible to declare custom properties higher in the cascade and let CSS inheritance solve the problem of making them available throughout a document. By declaring a custom property on the :root pseudo-class, you create a global variable that is available to every element in the document. For non-trivial projects, this is often the most practical approach for defining design tokens and theme values that need to be accessed everywhere.
The cascade resolution for custom properties follows the same rules as all other CSS properties--more specific selectors override less specific ones, and later declarations override earlier ones within the same specificity level. This means you can use custom property overrides to create themes, responsive variants, or component-specific styling without writing separate CSS rules for each scenario. The same property name can have different values in different contexts, all controlled by the cascade.
JavaScript Integration and Dynamic Updates
One of the most powerful features of CSS custom properties is their ability to be read and modified by JavaScript at runtime. This enables entirely new categories of dynamic styling that were previously impossible with preprocessor variables or even CSS-in-JS solutions that require style regeneration. You can create truly interactive experiences where styles respond instantly to user input without the overhead of re-rendering entire component trees.
This JavaScript API opens possibilities for implementing user preference systems, A/B testing visual variants, accessibility adjustments, and real-time visual customization. A website might read the user's system color scheme preference and set appropriate CSS variables, or allow users to customize their experience with instant visual feedback. The performance impact is minimal because these are native CSS properties being updated in the browser's CSSOM.
Practical Applications
Implementing dark mode toggles becomes straightforward--reading the user's system preference or responding to a button click by updating custom properties on the document root changes colors throughout the entire application instantly. A/B testing visual variants can use the same mechanism, showing different users different color schemes or spacing values without page reloads. For React applications, custom properties offer a performant way to update styles without triggering component re-renders or virtual DOM reconciliation.
When you update a custom property, the browser efficiently recalculates only the styles that depend on that property, maintaining smooth performance even in complex applications built with Next.js. This is more efficient than alternatives that require regenerating component styles or triggering full style recalculations.
1// Reading a custom property value2const styles = getComputedStyle(document.documentElement);3const primaryColor = styles.getPropertyValue('--primary-color');4 5// Setting a custom property value6document.documentElement.style.setProperty('--primary-color', '#ff6600');7 8// Setting with important flag9document.documentElement.style.setProperty('--primary-color', '#ff6600', 'important');10 11// Dark mode toggle example12function toggleDarkMode() {13 const isDark = document.body.classList.toggle('dark');14 document.documentElement.style.setProperty(15 '--background-primary',16 isDark ? '#1a1a1a' : '#ffffff'17 );18}Performance Benefits
CSS custom properties offer significant performance advantages over preprocessor variables and other value reuse strategies. Because they are native browser features rather than text substitution, custom properties participate in the browser's CSS parsing and caching mechanisms just like any other property. Modern browsers can optimize custom property access and computation, making them efficient even in large-scale applications with hundreds of variables defined.
The dynamic nature of custom properties can actually improve performance compared to alternatives that require JavaScript re-rendering. When you update a custom property on the document root, the browser efficiently recalculates only the styles that depend on that property rather than regenerating entire stylesheets or component trees. For Next.js applications and other React-based frameworks, this means styles can update smoothly without triggering component re-renders or virtual DOM reconciliation.
Reducing Stylesheet Size
Custom properties can significantly reduce stylesheet size by eliminating repetition. A typical color might appear dozens of times across a large codebase--each occurrence adds bytes to the final CSS file. By defining colors as variables and referencing them throughout, you pay the cost of the color value once and the variable reference multiple times. The var() function adds minimal overhead compared to the savings from eliminating repeated values.
Consider a website with three components using the same primary color--without custom properties, the hex value appears three times in the compiled CSS. With custom properties, you define the color once and reference it three times. If the brand color needs updating, you change it in one place rather than three, reducing both file size and maintenance overhead. This is especially valuable for performance optimization where reducing CSS payload directly impacts page load times and Core Web Vitals metrics.
Why Custom Properties Win
Runtime
Accessible via JavaScript
Native
No build tools required
Cascade
True inheritance support
Instant
Update without re-renders
Best Practices for Organization
Effective use of custom properties requires thoughtful organization and naming conventions. A well-structured approach separates concerns into different levels of abstraction--global design tokens, semantic variables, and component-specific overrides. This layered approach enables both consistency and flexibility, ensuring that related values change together while allowing targeted customization where needed.
Design token systems typically define the most primitive values: raw color values, spacing units, typography scales, and other foundational elements. These tokens often use implementation-agnostic names like --color-blue-500 or --spacing-4. Semantic tokens bridge the gap between design tokens and components, mapping tokens to meaningful purposes like --color-primary or --spacing-container. Component variables complete the picture, using semantic tokens to build specific component styles while allowing override through local custom property definitions.
Naming conventions for custom properties should be consistent and descriptive. Many teams use a specific pattern such as category-purpose-value (e.g., --color-brand-primary) or a functional naming approach like --text-color-body. Documenting these conventions and maintaining them across projects helps team members understand and predict variable names, improving collaboration and maintainability. The goal is to make variable names self-documenting--anyone reading the CSS should understand what values the variable controls.
Integration with Modern Frameworks
Modern frameworks like Next.js handle CSS custom properties elegantly, allowing you to define variables at the document level that cascade through all components. Whether you use CSS Modules, styled-components, or Tailwind CSS, custom properties integrate naturally as native CSS features. The key advantage is that these variables remain accessible even when component styles are isolated--the cascade bridges the boundaries between component styles.
For framework-specific approaches, you might define root variables in your global CSS file and reference them within component styles. This pattern works regardless of how components are styled internally, providing a consistent theming mechanism across your entire application. Tailwind CSS users can integrate custom properties by extending their theme configuration or using arbitrary values that reference CSS variables.
Container Queries and Responsive Design
Container queries represent another complementary feature that works naturally with custom properties. You can define responsive variables that change based on container dimensions rather than viewport size, enabling truly component-based responsive design. The combination of container queries and custom properties represents the cutting edge of CSS-based responsive design, allowing components to adapt to their available space regardless of page context.
Frequently Asked Questions
How are CSS custom properties different from Sass variables?
Sass variables are compiled away at build time--they're essentially find-and-replace operations. CSS custom properties are native browser features that exist in the CSSOM and can be read/modified by JavaScript at runtime. CSS variables also participate in the cascade and inheritance.
Can I use custom properties inside media queries?
You can reference custom properties inside media queries, but you cannot use variables as the media condition itself. For responsive values, define breakpoint-specific variables at different selector levels or use container queries for component-based responsiveness.
Do custom properties work in all browsers?
Custom properties are supported in all modern browsers including Chrome, Firefox, Safari, and Edge. For older browser support, consider providing fallback values using the var() function's second parameter.
Should I use @property or regular -- declarations?
Use regular -- declarations for simple cases where type inference is sufficient. Use @property when you need type validation, better developer experience with autocomplete, or when you want to disable inheritance with inherits: false.
Sources
-
MDN Web Docs - CSS Custom Properties for Cascading Variables - Official CSS module specification for cascading variables, property definitions, and function references.
-
MDN Web Docs - Using CSS Custom Properties - Practical usage guide including declaration methods, var() function, inheritance behavior, and JavaScript manipulation.