CSS Variables (Custom Properties)

Learning Objectives: By the end of this lesson, you will be able to:

  • Define and use CSS custom properties to create maintainable stylesheets
  • Understand variable scope and inheritance in CSS custom properties
  • Implement dynamic theming and responsive design using CSS variables
  • Combine CSS variables with JavaScript for interactive, data-driven styles

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Welcome! Think about decorating a room in your house - wouldn’t it be convenient if you could define a color scheme once and have it automatically apply to all your furniture, walls, and accessories? And if you wanted to change the theme, you’d only need to update it in one place! CSS variables work exactly like this for websites. If you could define a custom color for your whole website that perfectly represents your personality or brand, what would you name it and what color would it be?

Lesson Overview

  • Duration: 90 minutes
  • Prerequisites: Basic understanding of CSS properties, selectors, and the cascade
  • Materials Needed: Code editor, web browser with developer tools for inspecting variables

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. CSS Variables Fundamentals - Understanding custom properties and their syntax
  2. Variable Scope and Inheritance - Global vs. local variables and the cascade
  3. Dynamic Theming - Creating switchable color schemes and responsive variables
  4. JavaScript Integration - Making variables interactive and data-driven
  5. Best Practices - Organizing variables and building maintainable design systems

Success Criteria:

By the end of this session, you should be able to create a complete design system using CSS variables that’s easily maintainable, supports multiple themes, and can be dynamically updated with JavaScript.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Understanding CSS Variables vs. Traditional Values

Traditional CSS requires repeating values throughout your stylesheet. If you want to change a brand color, you might need to update it in dozens of places. CSS variables (officially called “custom properties”) solve this by letting you define a value once and reference it everywhere.

Real-World Analogy: Think of CSS variables like a phone contact list. Instead of memorizing everyone’s phone number, you save it once under their name and just dial their name. If their number changes, you update it once and it’s correct everywhere. CSS variables work the same way - define a color once as --brand-primary and use it throughout your site.

Why This Matters: CSS variables enable:

  • Maintainability: Change a value in one place to update it everywhere
  • Consistency: Ensure the same values are used across your entire project
  • Dynamic Updates: JavaScript can modify variables in real-time
  • Theming: Switch between different design schemes effortlessly

The Power of Custom Properties

Unlike preprocessor variables (Sass, Less), CSS custom properties are:

  • Live: They exist in the browser and can be changed at runtime
  • Cascading: They follow CSS inheritance and specificity rules
  • Inspectable: You can see them in browser developer tools
  • Responsive: They can change based on media queries
  • Scriptable: JavaScript can read and modify them dynamically

Browser Support and Fallbacks

CSS custom properties are well-supported in modern browsers (95%+ global support), but it’s important to understand how to provide fallbacks for older browsers when necessary.

Key Terminology

  • Custom Property: The official name for CSS variables, defined with --property-name
  • Variable Declaration: Defining a custom property with --name: value
  • Variable Usage: Accessing a custom property with var(--name)
  • Fallback Value: A backup value used when a variable is undefined
  • Scope: Where a variable is accessible (global, local, or inherited)
  • Cascade: How variables inherit and override each other

4. Detailed Explanation and Examples (25-30 minutes)

Understanding CSS Variable Syntax

CSS variables use a unique syntax that distinguishes them from regular CSS properties. They must start with two dashes (--) and can contain letters, numbers, hyphens, and underscores.

Variable Declaration Syntax

/* Basic syntax */
--variable-name: value;

/* Examples of valid variable names */
--primary-color: #3498db;
--font-size-large: 1.5rem;
--border-radius: 8px;
--animation-duration: 300ms;
--is-dark-theme: 1; /* Can store numbers */

Variable Usage with var()

/* Basic usage */
property: var(--variable-name);

/* With fallback value */
property: var(--variable-name, fallback-value);

/* Nested fallbacks */
property: var(--primary, var(--secondary, #000));

Understanding Variable Scope

CSS variables follow the cascade and inheritance, which makes them incredibly powerful for creating organized, hierarchical design systems.

Global Variables

Global variables are typically defined on the :root pseudo-class, making them available throughout the entire document.

:root {
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
  --text-color: #2c3e50;
  --background-color: #ecf0f1;
}

Local Variables and Override Patterns

Variables can be overridden in specific contexts, allowing for component-specific customization while maintaining global consistency.

/* Global definition */
:root {
  --button-bg: #3498db;
  --button-text: white;
}

/* Override for specific context */
.dark-theme {
  --button-bg: #34495e;
  --button-text: #ecf0f1;
}

.error-state {
  --button-bg: #e74c3c;
}

Inheritance and Cascade Behavior

CSS variables inherit from parent to child elements, following the same rules as other inherited CSS properties like color and font-family.

Common Misconceptions

  1. Misconception: CSS variables are the same as Sass/Less variables Reality: CSS variables are live in the browser and can be changed dynamically

  2. Misconception: Variables slow down CSS performance Reality: Modern browsers optimize variable usage, and the maintainability benefits far outweigh any minimal performance impact

  3. Misconception: You can’t use variables in media queries Reality: While you can’t use variables in media query conditions, you can change variable values within media queries

5. Live Coding Demonstration (20-25 minutes)

Example 1: Basic Variable Usage and Refactoring

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS Variables Basics</title>
    <style>
        /* Before: Repetitive CSS */
        /*
        .header {
            background: #3498db;
            padding: 20px;
            border-radius: 8px;
        }
        .button {
            background: #3498db;
            color: white;
            padding: 12px 20px;
            border-radius: 8px;
            border: none;
        }
        .card {
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(52, 152, 219, 0.2);
        }
        */

        /* After: Using CSS Variables */
        :root {
            --primary-color: #3498db;
            --primary-rgb: 52, 152, 219;
            --white: #ffffff;
            --border-radius: 8px;
            --spacing-md: 20px;
            --spacing-sm: 12px;
            --box-shadow: 0 2px 4px rgba(var(--primary-rgb), 0.2);
        }

        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f8f9fa;
        }

        .header {
            background: var(--primary-color);
            color: var(--white);
            padding: var(--spacing-md);
            border-radius: var(--border-radius);
            margin-bottom: var(--spacing-md);
        }

        .button {
            background: var(--primary-color);
            color: var(--white);
            padding: var(--spacing-sm) var(--spacing-md);
            border-radius: var(--border-radius);
            border: none;
            cursor: pointer;
            transition: opacity 0.3s ease;
        }

        .button:hover {
            opacity: 0.9;
        }

        .card {
            background: var(--white);
            padding: var(--spacing-md);
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
            margin-bottom: var(--spacing-md);
        }

        /* Demonstration of variable overriding */
        .success-button {
            --primary-color: #2ecc71;
        }

        .danger-button {
            --primary-color: #e74c3c;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>CSS Variables Demo</h1>
        <p>This header uses variables for color, spacing, and border-radius.</p>
    </div>

    <div class="card">
        <h2>Variable Benefits</h2>
        <p>Notice how changing the --primary-color variable would update the header, buttons, and shadows all at once.</p>
        
        <button class="button">Primary Button</button>
        <button class="button success-button">Success Button</button>
        <button class="button danger-button">Danger Button</button>
    </div>
</body>
</html>

Explanation:

  • Lines 26-34: Global variables defined on :root for reuse throughout the stylesheet
  • Line 28: RGB values stored separately for use in rgba() functions
  • Lines 52, 57, 62: Same button class with different color overrides using local variable scoping
  • Line 34: Complex values like box-shadows can also be stored in variables

Example 2: Dynamic Theme Switching

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dynamic Theme Switching</title>
    <style>
        :root {
            /* Light theme (default) */
            --bg-primary: #ffffff;
            --bg-secondary: #f8f9fa;
            --text-primary: #2c3e50;
            --text-secondary: #6c757d;
            --accent-color: #3498db;
            --border-color: #dee2e6;
            --shadow-color: rgba(0, 0, 0, 0.1);
        }

        [data-theme="dark"] {
            /* Dark theme overrides */
            --bg-primary: #2c3e50;
            --bg-secondary: #34495e;
            --text-primary: #ecf0f1;
            --text-secondary: #bdc3c7;
            --accent-color: #3498db;
            --border-color: #4a5568;
            --shadow-color: rgba(0, 0, 0, 0.3);
        }

        [data-theme="high-contrast"] {
            /* High contrast theme */
            --bg-primary: #000000;
            --bg-secondary: #1a1a1a;
            --text-primary: #ffffff;
            --text-secondary: #cccccc;
            --accent-color: #00ff00;
            --border-color: #ffffff;
            --shadow-color: rgba(255, 255, 255, 0.2);
        }

        * {
            transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
        }

        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background: var(--bg-secondary);
            color: var(--text-primary);
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
            background: var(--bg-primary);
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 6px var(--shadow-color);
        }

        .theme-switcher {
            margin-bottom: 30px;
            padding: 20px;
            background: var(--bg-secondary);
            border-radius: 8px;
            border: 1px solid var(--border-color);
        }

        .theme-button {
            background: var(--accent-color);
            color: var(--bg-primary);
            border: none;
            padding: 10px 20px;
            margin: 0 10px 10px 0;
            border-radius: 6px;
            cursor: pointer;
            font-weight: bold;
        }

        .theme-button:hover {
            opacity: 0.8;
        }

        .content-section {
            margin-bottom: 25px;
            padding: 20px;
            border: 1px solid var(--border-color);
            border-radius: 8px;
        }

        .content-section h2 {
            color: var(--text-primary);
            margin-top: 0;
        }

        .content-section p {
            color: var(--text-secondary);
            line-height: 1.6;
        }

        .highlight {
            background: var(--accent-color);
            color: var(--bg-primary);
            padding: 2px 6px;
            border-radius: 4px;
        }
    </style>
</head>
<body data-theme="light">
    <div class="container">
        <div class="theme-switcher">
            <h2>Theme Switcher</h2>
            <p>Click the buttons below to see how CSS variables enable instant theme switching:</p>
            <button class="theme-button" onclick="setTheme('light')">Light Theme</button>
            <button class="theme-button" onclick="setTheme('dark')">Dark Theme</button>
            <button class="theme-button" onclick="setTheme('high-contrast')">High Contrast</button>
        </div>

        <div class="content-section">
            <h2>Dynamic Styling with CSS Variables</h2>
            <p>This entire interface updates instantly when you change themes. All colors, backgrounds, and borders are controlled by <span class="highlight">CSS custom properties</span>.</p>
        </div>

        <div class="content-section">
            <h2>Benefits of Variable-Based Theming</h2>
            <p>Notice how the transition between themes is smooth thanks to CSS transitions. This creates a <span class="highlight">polished user experience</span> that would be difficult to achieve without variables.</p>
        </div>
    </div>

    <script>
        function setTheme(themeName) {
            document.body.setAttribute('data-theme', themeName);
            
            // Optional: Save theme preference to localStorage
            localStorage.setItem('preferred-theme', themeName);
        }

        // Load saved theme preference on page load
        document.addEventListener('DOMContentLoaded', function() {
            const savedTheme = localStorage.getItem('preferred-theme');
            if (savedTheme) {
                setTheme(savedTheme);
            }
        });
    </script>
</body>
</html>

Key Points:

  • Lines 10-16: Default light theme variables defined on :root
  • Lines 18-26: Dark theme overrides using attribute selector [data-theme="dark"]
  • Lines 28-36: High contrast theme for accessibility
  • Line 39: CSS transitions make theme switching smooth
  • Lines 113-125: JavaScript function updates the data attribute to switch themes

Example 3: Responsive Variables and Component System

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive CSS Variables</title>
    <style>
        :root {
            /* Typography scale */
            --font-size-xs: 0.75rem;
            --font-size-sm: 0.875rem;
            --font-size-base: 1rem;
            --font-size-lg: 1.125rem;
            --font-size-xl: 1.25rem;
            --font-size-2xl: 1.5rem;
            --font-size-3xl: 1.875rem;

            /* Spacing scale */
            --space-xs: 0.25rem;
            --space-sm: 0.5rem;
            --space-md: 1rem;
            --space-lg: 1.5rem;
            --space-xl: 2rem;
            --space-2xl: 3rem;

            /* Responsive breakpoints (for reference) */
            --bp-sm: 640px;
            --bp-md: 768px;
            --bp-lg: 1024px;

            /* Component-specific variables */
            --card-padding: var(--space-md);
            --card-border-radius: 8px;
            --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

            /* Button sizing */
            --btn-padding-y: var(--space-sm);
            --btn-padding-x: var(--space-md);
            --btn-font-size: var(--font-size-base);
        }

        /* Responsive variable adjustments */
        @media (min-width: 768px) {
            :root {
                --card-padding: var(--space-lg);
                --card-border-radius: 12px;
                --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                
                --btn-padding-y: var(--space-md);
                --btn-padding-x: var(--space-lg);
                --btn-font-size: var(--font-size-lg);
            }
        }

        @media (min-width: 1024px) {
            :root {
                --card-padding: var(--space-xl);
                --card-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
            }
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            margin: 0;
            padding: var(--space-md);
            background: #f7fafc;
            font-size: var(--font-size-base);
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        .grid {
            display: grid;
            gap: var(--space-md);
            grid-template-columns: 1fr;
        }

        @media (min-width: 768px) {
            .grid {
                grid-template-columns: repeat(2, 1fr);
                gap: var(--space-lg);
            }
        }

        @media (min-width: 1024px) {
            .grid {
                grid-template-columns: repeat(3, 1fr);
                gap: var(--space-xl);
            }
        }

        .card {
            background: white;
            padding: var(--card-padding);
            border-radius: var(--card-border-radius);
            box-shadow: var(--card-shadow);
            transition: all 0.3s ease;
        }

        .card:hover {
            --card-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
            transform: translateY(-2px);
        }

        .card h2 {
            margin: 0 0 var(--space-md) 0;
            font-size: var(--font-size-xl);
            color: #2d3748;
        }

        .card p {
            color: #4a5568;
            line-height: 1.6;
            margin-bottom: var(--space-lg);
        }

        .btn {
            display: inline-block;
            padding: var(--btn-padding-y) var(--btn-padding-x);
            font-size: var(--btn-font-size);
            background: #4299e1;
            color: white;
            text-decoration: none;
            border-radius: 6px;
            border: none;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }

        .btn:hover {
            background: #3182ce;
        }

        /* Component variations using variable overrides */
        .card-featured {
            --card-shadow: 0 10px 25px rgba(66, 153, 225, 0.15);
            border: 2px solid #4299e1;
        }

        .card-compact {
            --card-padding: var(--space-sm);
        }

        .btn-large {
            --btn-padding-y: var(--space-md);
            --btn-padding-x: var(--space-xl);
            --btn-font-size: var(--font-size-lg);
        }

        .btn-small {
            --btn-padding-y: var(--space-xs);
            --btn-padding-x: var(--space-sm);
            --btn-font-size: var(--font-size-sm);
        }
    </style>
</head>
<body>
    <div class="container">
        <h1 style="text-align: center; font-size: var(--font-size-3xl); margin-bottom: var(--space-2xl);">
            Responsive Design with CSS Variables
        </h1>

        <div class="grid">
            <div class="card">
                <h2>Standard Card</h2>
                <p>This card uses default variable values. Notice how the padding, shadows, and typography adjust automatically on different screen sizes.</p>
                <button class="btn">Learn More</button>
            </div>

            <div class="card card-featured">
                <h2>Featured Card</h2>
                <p>This card overrides the shadow variable to create a more prominent appearance while maintaining the responsive behavior.</p>
                <button class="btn btn-large">Get Started</button>
            </div>

            <div class="card card-compact">
                <h2>Compact Card</h2>
                <p>This card uses reduced padding for a more dense layout. The variable system makes this easy to achieve consistently.</p>
                <button class="btn btn-small">View Details</button>
            </div>
        </div>
    </div>
</body>
</html>

6. Hands-On Practice (30-40 minutes)

Exercise 1: Design System Creation (15 minutes)

Objective: Create a comprehensive design system using CSS variables

Instructions:

  1. Define a complete color palette with primary, secondary, and neutral colors
  2. Create a typography scale with consistent font sizes
  3. Establish a spacing system for margins and padding
  4. Build reusable button and card components

Starter Code:

:root {
    /* Define your design system variables here */
    
    /* Colors */
    /* Add primary, secondary, success, warning, error colors */
    
    /* Typography */
    /* Add font sizes from small to extra large */
    
    /* Spacing */
    /* Add consistent spacing values */
    
    /* Borders and shadows */
    /* Add border radius and box shadow values */
}

.button {
    /* Use variables to style a flexible button component */
}

.card {
    /* Use variables to create a consistent card component */
}

Solution:

:root {
    /* Color palette */
    --color-primary: #3b82f6;
    --color-secondary: #8b5cf6;
    --color-success: #10b981;
    --color-warning: #f59e0b;
    --color-error: #ef4444;
    --color-neutral-100: #f3f4f6;
    --color-neutral-500: #6b7280;
    --color-neutral-900: #111827;

    /* Typography scale */
    --text-xs: 0.75rem;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.25rem;
    --text-2xl: 1.5rem;

    /* Spacing scale */
    --spacing-1: 0.25rem;
    --spacing-2: 0.5rem;
    --spacing-3: 0.75rem;
    --spacing-4: 1rem;
    --spacing-6: 1.5rem;
    --spacing-8: 2rem;

    /* Component tokens */
    --border-radius: 0.375rem;
    --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.button {
    padding: var(--spacing-2) var(--spacing-4);
    background: var(--color-primary);
    color: white;
    border: none;
    border-radius: var(--border-radius);
    font-size: var(--text-base);
    cursor: pointer;
}

.button--secondary {
    --color-primary: var(--color-secondary);
}

.card {
    background: white;
    padding: var(--spacing-6);
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
}

Exercise 2: Dark/Light Theme Toggle (10 minutes)

Objective: Implement a complete theme switching system

Requirements:

  • Create light and dark theme color schemes
  • Add a toggle button that switches between themes
  • Ensure all components adapt to theme changes
  • Include smooth transitions between themes

Hints:

  • Use data attributes or CSS classes to control themes
  • Store user preference in localStorage
  • Consider accessibility with high contrast options

Exercise 3: Responsive Component System (15 minutes)

Objective: Build components that adapt using responsive CSS variables

Challenge: Create a card grid layout where:

  • Card padding increases on larger screens
  • Typography scales appropriately
  • Grid columns adjust based on screen size
  • All controlled through responsive CSS variables

Extension Ideas:

  • Add hover effects that modify variable values
  • Create size variants (small, medium, large) using variable overrides
  • Implement a preference system for users to choose their preferred spacing

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Variables not updating when changed via JavaScript Cause: Forgetting that variables need to be set on the correct element Solution: Use element.style.setProperty('--variable-name', 'value') and ensure the element has the variable in scope

  2. Problem: Variables not working in pseudo-elements Cause: Pseudo-elements don’t inherit variables unless explicitly set Solution: Ensure variables are defined on the parent element or :root

  3. Problem: Circular variable references causing issues Cause: Variables referencing themselves directly or indirectly Solution: Avoid circular references and use clear naming conventions

  4. Problem: Variables not working in older browsers Cause: CSS custom properties aren’t supported in IE Solution: Provide fallback values or use PostCSS plugins for legacy support

Debugging Tips

  • Use browser DevTools to inspect computed variable values
  • Check the “Computed” tab to see resolved variable values
  • Use the :root inspector to see all global variables
  • Add temporary debug colors to quickly identify variable issues
  • Use CSS comments to document complex variable hierarchies

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Naming Conventions: Use descriptive, semantic names (--color-primary instead of --blue)
  • Organization: Group related variables together and use comments for sections
  • Fallbacks: Always provide fallback values for critical styling
  • Performance: Variables have minimal performance impact, but avoid excessive nesting

CSS Variable Organization

:root {
    /* ================================
       DESIGN TOKENS
       ================================ */
    
    /* Color Palette */
    --color-brand-primary: #3b82f6;
    --color-brand-secondary: #8b5cf6;
    
    /* Semantic Colors */
    --color-text-primary: var(--color-neutral-900);
    --color-text-secondary: var(--color-neutral-600);
    --color-background: var(--color-neutral-50);
    
    /* Component Tokens */
    --button-bg-primary: var(--color-brand-primary);
    --button-text-primary: white;
    
    /* ================================
       SPACING & TYPOGRAPHY
       ================================ */
    
    --space-unit: 0.25rem;
    --space-xs: calc(var(--space-unit) * 1);
    --space-sm: calc(var(--space-unit) * 2);
    --space-md: calc(var(--space-unit) * 4);
}

Next Steps in Your Learning Journey

  • Study CSS Grid and Flexbox integration with variables
  • Learn about CSS-in-JS libraries that leverage custom properties
  • Explore design system methodologies and atomic design
  • Practice building complete theme systems for real projects

9. Summary and Review (5 minutes)

Key Takeaways

  1. CSS variables enable maintainable, dynamic stylesheets with values that can be reused and updated centrally
  2. Scope and inheritance allow for powerful override patterns and component-specific customization
  3. JavaScript integration makes variables perfect for interactive themes and data-driven styling
  4. Design systems built with variables are more consistent and easier to maintain

Quick Review Questions

  1. What’s the difference between CSS variables and preprocessor variables?
  2. How do you provide a fallback value when using a CSS variable?
  3. What CSS selector would you use to define global variables?

Answers:

  1. CSS variables are live in the browser and can be changed at runtime; preprocessor variables are compiled away
  2. Use the second parameter in var(): var(--variable-name, fallback-value)
  3. The :root pseudo-class is typically used for global variable definitions

For Instructors

Teaching Tips

  • Demonstrate variable changes live in browser DevTools to show their dynamic nature
  • Use real-world examples of design systems (Material Design, Bootstrap) to show variable usage
  • Emphasize the maintainability benefits with before/after code examples
  • Show how variables solve common CSS problems like theme switching

Assessment Ideas

  • Quick Quiz: Variable syntax, scope rules, and use cases
  • Design Challenge: Build a complete component library using only CSS variables
  • Refactoring Exercise: Convert a traditional CSS file to use variables

Adaptation Notes

  • For Beginners: Focus on basic usage and benefits before diving into complex scope rules
  • For Advanced Students: Include performance considerations and integration with CSS frameworks
  • Online Delivery: Use browser DevTools screen sharing to clearly show variable behavior
  • Time Management: Adjust exercise complexity based on student familiarity with design systems