Color Mode

CSS Custom Properties and Math Functions

CSS custom properties, often called CSS variables, are one of the most powerful features added to modern CSS. Unlike preprocessor variables that you just learned about, custom properties are live values that exist in the browser, cascade through the DOM, and can change dynamically at runtime. Combined with CSS math functions like calc(), clamp(), min(), and max(), they enable sophisticated design systems and responsive layouts that adapt intelligently to context.

This learning activity explores how custom properties work, how they differ from preprocessor variables, and how to leverage them alongside math functions to build flexible, maintainable stylesheets.

What Are CSS Custom Properties?

CSS custom properties are variables defined in CSS that can store values for reuse throughout your stylesheet. They are declared using the -- prefix and accessed using the var() function.


      :root {
          --primary-color: #3498db;
          --spacing-unit: 1rem;
      }
      
      .button {
          background-color: var(--primary-color);
          padding: var(--spacing-unit);
      }
  

At first glance, this looks similar to Sass or Less variables. However, custom properties have unique characteristics that make them fundamentally different and more powerful in many scenarios.

Key Differences from Preprocessor Variables

Preprocessor variables are compile-time constants. When Sass or Less processes your code, it replaces variable references with their values, and those values are baked into the output CSS. Custom properties, by contrast, are runtime values that exist in the browser. This creates several important differences:

These characteristics make custom properties ideal for theming, responsive design, and building component systems that adapt to their context.

Declaring and Using Custom Properties

Global Declaration

The most common pattern is to declare custom properties on the :root pseudo-class, making them available throughout your entire document.


      :root {
          --primary-color: #3498db;
          --secondary-color: #2ecc71;
          --danger-color: #e74c3c;
          
          --font-size-base: 1rem;
          --font-size-large: 1.25rem;
          --font-size-xlarge: 1.5rem;
          
          --spacing-xs: 0.25rem;
          --spacing-sm: 0.5rem;
          --spacing-md: 1rem;
          --spacing-lg: 1.5rem;
          --spacing-xl: 2rem;
      }
  

This establishes a design system of reusable values. Any element in your document can reference these properties using the var() function.

Using Custom Properties

The var() function retrieves the value of a custom property. It accepts two arguments: the property name and an optional fallback value.


      .button {
          /* Use the custom property */
          background-color: var(--primary-color);
          padding: var(--spacing-md);
          font-size: var(--font-size-base);
      }
      
      .alert {
          /* Use with fallback in case the property is not defined */
          border-color: var(--border-color, #ddd);
      }
  

The fallback value ensures your styles degrade gracefully if a custom property is undefined. This is particularly useful when building reusable components that might be used in different contexts.

Scoped Custom Properties

Custom properties can be scoped to specific elements, allowing you to create variations without defining new global variables.


      :root {
          --button-bg: #3498db;
          --button-color: white;
      }
      
      .button {
          background-color: var(--button-bg);
          color: var(--button-color);
          padding: var(--spacing-md);
          border: none;
          border-radius: 0.25rem;
      }
      
      /* Scoped variation for danger buttons */
      .button-danger {
          --button-bg: #e74c3c;
      }
      
      /* Scoped variation for success buttons */
      .button-success {
          --button-bg: #2ecc71;
      }
  

In this example, .button-danger and .button-success redefine the --button-bg property locally. The .button class references var(--button-bg), so it automatically picks up the scoped value. This pattern reduces code duplication and makes variations easy to create.

Cascade and Inheritance

One of the most powerful features of custom properties is that they participate in the CSS cascade. Unlike preprocessor variables, which are simply replaced with their values at compile time, custom properties can be overridden at different levels of specificity and can inherit through the DOM tree.

How Inheritance Works

Custom properties inherit from parent elements, just like properties such as color or font-family.


      .theme-dark {
          --text-color: #f0f0f0;
          --bg-color: #1a1a1a;
      }
      
      .theme-light {
          --text-color: #333;
          --bg-color: #ffffff;
      }
      
      .content {
          color: var(--text-color);
          background-color: var(--bg-color);
      }
  

      <!-- This content will use dark theme colors -->
      <div class="theme-dark">
          <div class="content">Dark themed content</div>
      </div>
      
      <!-- This content will use light theme colors -->
      <div class="theme-light">
          <div class="content">Light themed content</div>
      </div>
  

The .content elements inherit custom property values from their parent themes. This makes theming incredibly flexible because you can change an entire section of your page by applying a single class to a parent element.

Specificity and Overriding

Custom properties follow normal CSS specificity rules. More specific selectors can override less specific ones.


      :root {
          --spacing: 1rem;
      }
      
      .compact {
          --spacing: 0.5rem;
      }
      
      .compact.extra-compact {
          --spacing: 0.25rem;
      }
      
      .card {
          padding: var(--spacing);
      }
  

A card with class="card compact extra-compact" will use 0.25rem padding because the more specific selector wins. This cascading behavior enables sophisticated component variations.

Practical Application: Theming Systems

Custom properties excel at building theme systems because they can change dynamically based on context. A common pattern is creating light and dark modes that users can toggle.

Basic Theme Implementation


      :root {
          /* Default light theme */
          --bg-primary: #ffffff;
          --bg-secondary: #f5f5f5;
          --text-primary: #333333;
          --text-secondary: #666666;
          --border-color: #dddddd;
          --shadow: rgba(0, 0, 0, 0.1);
      }
      
      [data-theme="dark"] {
          --bg-primary: #1a1a1a;
          --bg-secondary: #2d2d2d;
          --text-primary: #f0f0f0;
          --text-secondary: #b0b0b0;
          --border-color: #444444;
          --shadow: rgba(0, 0, 0, 0.5);
      }
      
      body {
          background-color: var(--bg-primary);
          color: var(--text-primary);
          transition: background-color 0.3s, color 0.3s;
      }
      
      .card {
          background-color: var(--bg-secondary);
          border: 1px solid var(--border-color);
          box-shadow: 0 2px 4px var(--shadow);
          color: var(--text-primary);
      }
      
      .card-title {
          color: var(--text-primary);
      }
      
      .card-subtitle {
          color: var(--text-secondary);
      }
  

To switch themes, simply toggle the data-theme attribute on the root element (typically with JavaScript). All elements automatically update because they reference the custom properties, which cascade down from the root.

Media Query Integration

You can respect user preferences by using the prefers-color-scheme media query to set the default theme.


      :root {
          /* Light theme by default */
          --bg-primary: #ffffff;
          --text-primary: #333333;
      }
      
      @media (prefers-color-scheme: dark) {
          :root {
              /* Dark theme for users who prefer it */
              --bg-primary: #1a1a1a;
              --text-primary: #f0f0f0;
          }
      }
  

This automatically applies dark mode for users whose system preferences indicate they prefer dark interfaces, while still allowing manual override if needed.

Component-Specific Theming

Custom properties enable component-level theming without polluting the global scope.


      .alert {
          --alert-bg: var(--bg-secondary);
          --alert-border: var(--border-color);
          --alert-text: var(--text-primary);
          
          background-color: var(--alert-bg);
          border: 1px solid var(--alert-border);
          color: var(--alert-text);
          padding: var(--spacing-md);
          border-radius: 0.25rem;
      }
      
      .alert-info {
          --alert-bg: #d1ecf1;
          --alert-border: #bee5eb;
          --alert-text: #0c5460;
      }
      
      .alert-warning {
          --alert-bg: #fff3cd;
          --alert-border: #ffeaa7;
          --alert-text: #856404;
      }
      
      .alert-danger {
          --alert-bg: #f8d7da;
          --alert-border: #f5c6cb;
          --alert-text: #721c24;
      }
  

Each alert variant redefines the component-level custom properties. The base .alert class references these properties, making variants clean and maintainable.

CSS Math Functions

CSS includes several math functions that enable dynamic calculations right in your stylesheets. These functions work seamlessly with custom properties, creating powerful responsive design patterns.

calc(): The Foundation

The calc() function performs basic arithmetic operations: addition, subtraction, multiplication, and division. Its superpower is mixing different units in the same calculation.


      .sidebar {
          /* Mix percentage and fixed units */
          width: calc(100% - 250px);
      }
      
      .header {
          /* Use custom properties in calculations */
          padding: calc(var(--spacing-md) * 2);
      }
      
      .container {
          /* Complex calculations */
          max-width: calc(100vw - (var(--spacing-lg) * 2));
          margin: 0 auto;
      }
  

This flexibility makes calc() essential for responsive layouts. You can create fluid spacing, size elements relative to their containers, and build designs that adapt intelligently.

clamp(): Responsive Values with Boundaries

The clamp() function is one of the most powerful tools for responsive design. It takes three values: a minimum, a preferred value, and a maximum. The browser calculates the preferred value and constrains it within the minimum and maximum bounds.


      h1 {
          /* Font size grows with viewport but stays within bounds */
          font-size: clamp(1.5rem, 4vw, 3rem);
      }
  

In this example:

This creates fluid typography that adapts smoothly across all screen sizes without media queries.

Fluid Spacing with clamp()

Use clamp() for spacing that scales with viewport size but remains readable on all devices.


      :root {
          --spacing-fluid-sm: clamp(0.5rem, 2vw, 1rem);
          --spacing-fluid-md: clamp(1rem, 4vw, 2rem);
          --spacing-fluid-lg: clamp(2rem, 6vw, 4rem);
      }
      
      .section {
          padding: var(--spacing-fluid-lg) var(--spacing-fluid-md);
      }
      
      .card {
          margin-bottom: var(--spacing-fluid-md);
      }
  

This creates a spacing system that feels natural across all viewport sizes, maintaining visual hierarchy without rigid breakpoints.

min() and max(): Conditional Sizing

The min() function returns the smallest value from a comma-separated list. The max() function returns the largest.


      .container {
          /* Width is either 100% or 1200px, whichever is smaller */
          width: min(100%, 1200px);
          margin: 0 auto;
      }
      
      .image {
          /* Height is at least 200px, but grows with width */
          height: max(200px, 50vw);
      }
  

These functions provide cleaner alternatives to complex media queries for simple conditional sizing. Use min() to set a maximum effective size and max() to set a minimum.

Combining Custom Properties and Math Functions

The real power emerges when you combine custom properties with math functions. This enables sophisticated design systems that adapt intelligently to context and viewport size.

Fluid Type Scale

Create a complete typographic scale that adapts to viewport size while maintaining hierarchy.


      :root {
          --font-size-base: clamp(1rem, 2.5vw, 1.125rem);
          
          /* Scale ratios using calc() and custom properties */
          --font-size-sm: calc(var(--font-size-base) * 0.875);
          --font-size-lg: calc(var(--font-size-base) * 1.25);
          --font-size-xl: calc(var(--font-size-base) * 1.5);
          --font-size-2xl: calc(var(--font-size-base) * 2);
          --font-size-3xl: calc(var(--font-size-base) * 2.5);
      }
      
      body {
          font-size: var(--font-size-base);
      }
      
      small {
          font-size: var(--font-size-sm);
      }
      
      h4 {
          font-size: var(--font-size-lg);
      }
      
      h3 {
          font-size: var(--font-size-xl);
      }
      
      h2 {
          font-size: var(--font-size-2xl);
      }
      
      h1 {
          font-size: var(--font-size-3xl);
      }
  

The base font size scales with the viewport using clamp(), and all other sizes calculate from it using calc(). Change one variable and the entire scale adapts proportionally.

Responsive Component System

Build components that resize intelligently based on their container and viewport.


      :root {
          --container-padding: clamp(1rem, 5vw, 3rem);
          --gap-size: clamp(1rem, 3vw, 2rem);
          --column-min-width: 300px;
      }
      
      .grid {
          display: grid;
          gap: var(--gap-size);
          /* Columns are at least 300px, but grow to fill space */
          grid-template-columns: repeat(
              auto-fit,
              minmax(var(--column-min-width), 1fr)
          );
          padding: var(--container-padding);
      }
      
      .card {
          padding: calc(var(--container-padding) * 0.5);
          border-radius: clamp(0.5rem, 1vw, 1rem);
      }
  

This grid adapts its spacing, padding, and column sizing fluidly across all viewport sizes. The custom properties establish relationships between measurements, and the math functions ensure they scale appropriately.

Context-Aware Spacing

Create spacing that adapts based on where components are used.


      :root {
          --spacing-multiplier: 1;
      }
      
      .compact {
          --spacing-multiplier: 0.5;
      }
      
      .spacious {
          --spacing-multiplier: 1.5;
      }
      
      .section {
          padding: calc(var(--spacing-md) * var(--spacing-multiplier));
          margin-bottom: calc(var(--spacing-lg) * var(--spacing-multiplier));
      }
      
      .section > * + * {
          margin-top: calc(var(--spacing-sm) * var(--spacing-multiplier));
      }
  

Applying .compact or .spacious to a parent element scales all descendant spacing proportionally. This pattern enables density variations without rewriting spacing rules.

JavaScript Integration

Because custom properties are live values in the browser, JavaScript can read and modify them dynamically. This enables interactive features and runtime customization that preprocessor variables cannot provide.

Reading Custom Properties


      // Get the computed value of a custom property
      const root = document.documentElement;
      const primaryColor = getComputedStyle(root)
          .getPropertyValue('--primary-color');
      
      console.log(primaryColor); // "#3498db"
  

Writing Custom Properties


      // Set a custom property value
      const root = document.documentElement;
      root.style.setProperty('--primary-color', '#e74c3c');
      
      // All elements using var(--primary-color) update immediately
  

Interactive Theme Switching


      // Toggle between light and dark themes
      function toggleTheme() {
          const root = document.documentElement;
          const currentTheme = root.getAttribute('data-theme');
          const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
          root.setAttribute('data-theme', newTheme);
      }
      
      // Attach to a button
      document.querySelector('.theme-toggle')
          .addEventListener('click', toggleTheme);
  

This simple JavaScript, combined with the CSS custom properties you defined earlier, creates a fully functional theme switcher. The browser handles all the style updates automatically.

Dynamic User Preferences


      // Let users customize their interface
      function setFontSize(size) {
          document.documentElement.style
              .setProperty('--font-size-base', `${size}px`);
      }
      
      // Font size slider
      document.querySelector('.font-size-slider')
          .addEventListener('input', (e) => {
              setFontSize(e.target.value);
          });
  

This pattern enables user preferences without regenerating CSS or maintaining multiple stylesheets. The entire design system adjusts because all sizes calculate from the base value.

Why JavaScript Integration Matters

The ability to manipulate custom properties with JavaScript creates possibilities that preprocessors cannot match. You can build interactive themes, user customization features, and dynamic design systems that respond to user actions or application state, all while keeping your CSS clean and maintainable.

Best Practices and Patterns

Naming Conventions

Use clear, descriptive names that indicate purpose and scope. Common patterns include:

Organization Strategy

Group related custom properties together and document their purpose.


      :root {
          /* Colors */
          --color-primary: #3498db;
          --color-secondary: #2ecc71;
          --color-danger: #e74c3c;
          
          /* Spacing */
          --spacing-xs: 0.25rem;
          --spacing-sm: 0.5rem;
          --spacing-md: 1rem;
          --spacing-lg: 1.5rem;
          --spacing-xl: 2rem;
          
          /* Typography */
          --font-size-base: 1rem;
          --line-height-base: 1.5;
          
          /* Layout */
          --container-max-width: 1200px;
          --sidebar-width: 250px;
      }
  

Progressive Enhancement

Always provide fallback values for browsers that might not support certain features.


      .heading {
          /* Fallback for browsers without clamp() support */
          font-size: 2rem;
          
          /* Progressive enhancement with clamp() */
          font-size: clamp(1.5rem, 4vw, 3rem);
      }
  

Performance Considerations

Custom properties are efficient, but consider these guidelines:

Putting It All Together

CSS custom properties and math functions transform how you write stylesheets. They enable design systems that are flexible, maintainable, and responsive without heavy frameworks or complex build processes. Unlike preprocessor variables that lock in values at compile time, custom properties adapt dynamically to context, user preferences, and viewport size.

As you continue through this course, you will see these techniques applied in increasingly sophisticated ways. Custom properties form the foundation of modern CSS architecture, enabling theming, component systems, and responsive patterns that scale from simple projects to large applications.

Practice using custom properties in your projects. Start with simple color schemes and spacing systems, then experiment with fluid typography and component theming. The more you work with these features, the more natural they will become, and the more creative solutions you will discover.