CSS3 & Responsive Design

CSS Custom Properties (Variables)

45 min Lesson 19 of 60

Introduction to CSS Custom Properties

CSS Custom Properties, commonly known as CSS variables, are one of the most powerful features introduced in modern CSS. They allow you to define reusable values that can be referenced throughout your stylesheets, making your code more maintainable, flexible, and dynamic. Unlike preprocessor variables from tools like Sass or Less, CSS custom properties are live in the browser -- they participate in the cascade, can be inherited by child elements, and can even be manipulated with JavaScript at runtime.

Before custom properties existed, developers had to rely on preprocessor variables or manually search and replace values across large stylesheets. If you wanted to change your primary brand color, you might have needed to update dozens of declarations scattered across multiple files. CSS custom properties solve this problem at the language level, giving you a native mechanism for creating a single source of truth for repeated values.

Declaring Custom Properties

A custom property is declared using a name that begins with two hyphens (--). This double-hyphen prefix is what distinguishes custom properties from regular CSS properties. The name can contain letters, numbers, hyphens, and underscores, and it is case-sensitive -- meaning --my-Color and --my-color are two different properties.

Example: Declaring Custom Properties

:root {
    --color-primary: #3498db;
    --color-secondary: #2ecc71;
    --color-danger: #e74c3c;
    --font-family-base: 'Segoe UI', Tahoma, Geneva, sans-serif;
    --font-size-base: 16px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 32px;
    --border-radius: 4px;
    --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.1);
}

In the example above, the :root pseudo-class is used as the declaration scope. The :root selector matches the root element of the document, which is the <html> element in HTML. Because custom properties are inherited, declaring them on :root makes them available to every element in the document. This is the conventional approach for global variables.

Note: Custom property names are case-sensitive. --Color-Primary and --color-primary are treated as completely different properties. Stick to a consistent naming convention -- lowercase with hyphens is the most common standard.

Using the var() Function

To use a custom property, you reference it with the var() function. The var() function takes the custom property name as its first argument and returns the value assigned to that property. If the property is not defined or is invalid, the browser will either use the inherited value or the property's initial value.

Example: Using var() to Apply Custom Properties

body {
    font-family: var(--font-family-base);
    font-size: var(--font-size-base);
    color: var(--color-text);
    background-color: var(--color-background);
}

.btn-primary {
    background-color: var(--color-primary);
    color: #ffffff;
    padding: var(--spacing-sm) var(--spacing-md);
    border-radius: var(--border-radius);
    border: none;
    cursor: pointer;
}

.btn-danger {
    background-color: var(--color-danger);
    color: #ffffff;
    padding: var(--spacing-sm) var(--spacing-md);
    border-radius: var(--border-radius);
    border: none;
    cursor: pointer;
}

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

Notice how both .btn-primary and .btn-danger share the same padding, border-radius, and structure -- the only difference is the background color. By using variables for the shared values, any future changes to spacing or border radius only need to happen in one place.

Fallback Values in var()

The var() function accepts an optional second argument -- a fallback value. This fallback is used if the custom property is not defined, has been removed, or contains an invalid value. Fallback values are especially useful when building reusable components that might be used in different contexts where certain variables may not be defined.

Example: Fallback Values

/* Single fallback value */
.header {
    background-color: var(--header-bg, #333333);
    color: var(--header-text, #ffffff);
    padding: var(--header-padding, 20px);
}

/* Nested var() as fallback -- a variable falling back to another variable */
.sidebar {
    width: var(--sidebar-width, var(--default-sidebar-width, 250px));
}

/* Multiple values in a single fallback */
.content {
    font-family: var(--font-stack, 'Helvetica Neue', Arial, sans-serif);
}

/* Fallback with calc() */
.container {
    max-width: var(--container-width, calc(100% - 40px));
}
Tip: Everything after the first comma inside var() is treated as the fallback value. So var(--font, Helvetica, Arial, sans-serif) has a fallback of Helvetica, Arial, sans-serif -- the commas are part of the fallback, not additional arguments.

Scope: Global vs Local Variables

One of the most powerful aspects of CSS custom properties is their scoping behavior. Variables defined on :root are global and available everywhere. However, you can also define variables on any element, and those variables will only be available to that element and its descendants. This local scoping enables component-level theming and encapsulation.

Example: Global and Local Scope

/* Global variables available everywhere */
:root {
    --color-primary: #3498db;
    --color-text: #333333;
    --spacing-unit: 8px;
}

/* Local variables scoped to the alert component */
.alert {
    --alert-padding: 16px;
    --alert-border-width: 4px;
    padding: var(--alert-padding);
    border-left: var(--alert-border-width) solid var(--alert-border-color, #ccc);
    background-color: var(--alert-bg, #f5f5f5);
    color: var(--alert-text, var(--color-text));
}

/* Override local variables for specific alert types */
.alert-success {
    --alert-border-color: #2ecc71;
    --alert-bg: #eafaf1;
    --alert-text: #1e8449;
}

.alert-error {
    --alert-border-color: #e74c3c;
    --alert-bg: #fdedec;
    --alert-text: #c0392b;
}

.alert-warning {
    --alert-border-color: #f39c12;
    --alert-bg: #fef9e7;
    --alert-text: #9a7d0a;
}

In this example, the .alert class defines its own local variables with fallback values. The variant classes (.alert-success, .alert-error, .alert-warning) only need to override the variable values -- the actual CSS properties in the base .alert class remain untouched. This pattern is extremely clean and scalable.

Cascading and Inheritance of Variables

CSS custom properties follow the same cascade and inheritance rules as any other CSS property. A variable defined on a parent element is inherited by all its children. If a child element redefines the same variable, the new value takes effect for that child and its own descendants, without affecting the parent or siblings.

Example: Inheritance and Override Behavior

:root {
    --text-color: #333333;
    --bg-color: #ffffff;
}

body {
    color: var(--text-color);
    background-color: var(--bg-color);
}

/* The sidebar redefines the variables for itself and its children */
.sidebar {
    --text-color: #ffffff;
    --bg-color: #2c3e50;
    color: var(--text-color);
    background-color: var(--bg-color);
}

/* Elements inside .sidebar inherit the overridden values */
.sidebar a {
    color: var(--text-color);  /* Will be #ffffff, not #333333 */
}

/* Elements outside .sidebar still use the root values */
.main-content a {
    color: var(--text-color);  /* Will be #333333 */
}
Note: This inheritance behavior is fundamentally different from preprocessor variables, which are resolved at compile time and have no concept of DOM inheritance. CSS custom properties are resolved at runtime based on the element's position in the DOM tree.

Naming Conventions

Consistent naming is essential for maintainability, especially in large projects with many variables. The most common convention uses lowercase words separated by hyphens, with a category prefix that groups related variables together. Here are some widely-adopted naming patterns:

  • Color tokens: --color-primary, --color-secondary, --color-success, --color-danger, --color-text, --color-bg
  • Spacing tokens: --spacing-xs, --spacing-sm, --spacing-md, --spacing-lg, --spacing-xl
  • Typography tokens: --font-family-base, --font-family-heading, --font-size-sm, --font-size-base, --font-size-lg
  • Component-scoped: --card-padding, --card-bg, --btn-height, --nav-width
  • State modifiers: --color-primary-hover, --color-primary-active, --btn-disabled-opacity

Some teams use a namespace prefix like --app- or --ds- (design system) to avoid collisions with third-party libraries. For example: --ds-color-primary, --ds-spacing-md.

Theming with Variables: Color Schemes

One of the most popular use cases for CSS custom properties is theming. By defining your entire color palette as variables, you can switch between themes simply by redefining those variables under a different selector or class. This is how most modern dark mode implementations work.

Example: Light and Dark Theme with CSS Variables

/* Light theme (default) */
:root {
    --color-bg: #ffffff;
    --color-surface: #f8f9fa;
    --color-text: #212529;
    --color-text-secondary: #6c757d;
    --color-primary: #0d6efd;
    --color-primary-hover: #0b5ed7;
    --color-border: #dee2e6;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
}

/* Dark theme */
[data-theme="dark"] {
    --color-bg: #1a1a2e;
    --color-surface: #16213e;
    --color-text: #e0e0e0;
    --color-text-secondary: #a0a0a0;
    --color-primary: #4dabf7;
    --color-primary-hover: #74c0fc;
    --color-border: #2d2d44;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
}

/* Components automatically adapt to the active theme */
body {
    background-color: var(--color-bg);
    color: var(--color-text);
}

.card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-sm);
    border-radius: 8px;
    padding: 24px;
}

.card:hover {
    box-shadow: var(--shadow-md);
}

a {
    color: var(--color-primary);
}

a:hover {
    color: var(--color-primary-hover);
}

With this setup, toggling between light and dark mode only requires adding or removing the data-theme="dark" attribute on the root element. Every component that uses the variables will automatically update its appearance -- no additional CSS rules needed for each component.

Overriding Variables in Media Queries

CSS custom properties can be redefined inside media queries, allowing you to adjust your design tokens based on screen size, user preferences, or other media features. This is particularly powerful for responsive typography, spacing adjustments, and respecting the user's system-level theme preference.

Example: Responsive Variables with Media Queries

:root {
    --font-size-base: 14px;
    --font-size-h1: 28px;
    --spacing-section: 40px;
    --container-padding: 16px;
    --grid-columns: 1;
}

/* Tablet and above */
@media (min-width: 768px) {
    :root {
        --font-size-base: 16px;
        --font-size-h1: 36px;
        --spacing-section: 60px;
        --container-padding: 24px;
        --grid-columns: 2;
    }
}

/* Desktop and above */
@media (min-width: 1200px) {
    :root {
        --font-size-base: 18px;
        --font-size-h1: 48px;
        --spacing-section: 80px;
        --container-padding: 32px;
        --grid-columns: 3;
    }
}

/* Respect user's dark mode preference */
@media (prefers-color-scheme: dark) {
    :root {
        --color-bg: #1a1a2e;
        --color-text: #e0e0e0;
        --color-surface: #16213e;
    }
}

/* Respect user's reduced motion preference */
@media (prefers-reduced-motion: reduce) {
    :root {
        --transition-duration: 0s;
        --animation-duration: 0s;
    }
}

/* Using the responsive variables */
body {
    font-size: var(--font-size-base);
}

h1 {
    font-size: var(--font-size-h1);
}

section {
    padding: var(--spacing-section) 0;
}

.grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-columns), 1fr);
    gap: var(--container-padding);
}
Tip: By changing variables inside media queries rather than writing new property declarations for every element, you keep your responsive CSS much cleaner. The actual component styles remain unchanged -- only the variable values adapt.

Overriding Variables in Component Contexts

Custom properties are especially useful when you want different instances of the same component to look different depending on where they appear. Instead of creating modifier classes that duplicate the entire ruleset, you can simply override the relevant variables in each context.

Example: Contextual Component Overrides

/* Base button using variables */
.btn {
    --btn-bg: var(--color-primary);
    --btn-text: #ffffff;
    --btn-padding-x: 16px;
    --btn-padding-y: 8px;
    --btn-font-size: 14px;

    background-color: var(--btn-bg);
    color: var(--btn-text);
    padding: var(--btn-padding-y) var(--btn-padding-x);
    font-size: var(--btn-font-size);
    border: none;
    border-radius: var(--border-radius);
    cursor: pointer;
}

/* In the hero section, buttons are larger */
.hero .btn {
    --btn-padding-x: 32px;
    --btn-padding-y: 16px;
    --btn-font-size: 18px;
}

/* In the sidebar, buttons are smaller and have different colors */
.sidebar .btn {
    --btn-bg: transparent;
    --btn-text: var(--color-primary);
    --btn-padding-x: 12px;
    --btn-padding-y: 6px;
    --btn-font-size: 12px;
}

/* In the footer, buttons match the footer theme */
.footer .btn {
    --btn-bg: #ffffff;
    --btn-text: #333333;
}

This pattern eliminates the need for classes like .btn-large, .btn-small, or .btn-sidebar. The component naturally adapts based on its position in the DOM, which is far more maintainable than managing a growing list of modifier classes.

Using Variables with calc()

CSS custom properties work seamlessly with the calc() function, enabling mathematical operations based on variable values. This is extremely useful for creating proportional spacing systems, fluid typography, and dynamic layouts.

Example: Variables with calc()

:root {
    --spacing-unit: 8px;
    --font-size-base: 16px;
    --line-height-ratio: 1.5;
    --container-max-width: 1200px;
    --sidebar-width: 280px;
}

/* Proportional spacing using calc and a base unit */
.section {
    padding: calc(var(--spacing-unit) * 4) calc(var(--spacing-unit) * 2);
    margin-bottom: calc(var(--spacing-unit) * 6);
}

/* Fluid typography */
h1 {
    font-size: calc(var(--font-size-base) * 2.5);
    line-height: calc(var(--font-size-base) * 2.5 * var(--line-height-ratio));
}

h2 {
    font-size: calc(var(--font-size-base) * 2);
}

h3 {
    font-size: calc(var(--font-size-base) * 1.5);
}

/* Dynamic layout calculations */
.main-content {
    width: calc(100% - var(--sidebar-width) - var(--spacing-unit) * 4);
    margin-left: calc(var(--sidebar-width) + var(--spacing-unit) * 2);
}

/* Negative margin using calc */
.full-bleed {
    margin-left: calc(-1 * var(--container-padding));
    margin-right: calc(-1 * var(--container-padding));
    padding-left: var(--container-padding);
    padding-right: var(--container-padding);
}
Warning: The calc() function requires spaces around the + and - operators. Writing calc(var(--a)-var(--b)) will not work -- you must write calc(var(--a) - var(--b)). The * and / operators do not require spaces, but adding them improves readability.

Dynamic Updates with JavaScript

One of the biggest advantages CSS custom properties have over preprocessor variables is that they exist at runtime and can be read and modified with JavaScript. This opens up possibilities for dynamic theming, user preferences, scroll-based animations, and real-time style manipulation without needing to modify CSS classes.

Example: Reading and Setting Variables with JavaScript

/* CSS */
:root {
    --primary-hue: 210;
    --primary-saturation: 70%;
    --primary-lightness: 50%;
    --color-primary: hsl(
        var(--primary-hue),
        var(--primary-saturation),
        var(--primary-lightness)
    );
}

/* JavaScript */

// Reading a custom property value
const root = document.documentElement;
const styles = getComputedStyle(root);
const currentHue = styles.getPropertyValue('--primary-hue').trim();
console.log('Current hue:', currentHue);  // "210"

// Setting a custom property value
root.style.setProperty('--primary-hue', '150');

// Toggling dark mode
function toggleDarkMode() {
    const html = document.documentElement;
    const isDark = html.getAttribute('data-theme') === 'dark';
    html.setAttribute('data-theme', isDark ? 'light' : 'dark');
}

// Dynamic color picker
const colorPicker = document.getElementById('color-picker');
colorPicker.addEventListener('input', function(e) {
    document.documentElement.style.setProperty(
        '--color-primary',
        e.target.value
    );
});

// Responsive font size based on slider
const slider = document.getElementById('font-slider');
slider.addEventListener('input', function(e) {
    document.documentElement.style.setProperty(
        '--font-size-base',
        e.target.value + 'px'
    );
});

The getComputedStyle() method reads the current resolved value of a custom property, while style.setProperty() sets a new value on the element's inline styles. Inline styles have higher specificity than stylesheet rules, so the new value will take effect immediately and propagate to all descendants that use the variable.

CSS Variables vs Preprocessor Variables

It is important to understand the differences between CSS custom properties and preprocessor variables (Sass, Less, Stylus), as they serve overlapping but distinct purposes:

  • Resolution time: Preprocessor variables are resolved at compile time and output static CSS. CSS custom properties are resolved at runtime by the browser.
  • Cascade and inheritance: CSS custom properties participate in the cascade and are inherited. Preprocessor variables have no concept of the DOM or cascade.
  • JavaScript access: CSS custom properties can be read and modified with JavaScript. Preprocessor variables cannot be accessed at runtime.
  • Media queries: CSS custom properties can be redefined inside media queries and the change applies dynamically. Preprocessor variables used in media queries produce static values at compile time.
  • Scope: CSS custom properties are scoped to elements and their descendants. Preprocessor variables are scoped to blocks or files.
  • Browser support: CSS custom properties require a modern browser (supported in all current browsers). Preprocessor variables have no browser requirement since they compile to standard CSS.
  • Fallback: CSS custom properties support fallback values with var(). Preprocessor variables use default parameter syntax specific to the preprocessor.

In practice, many teams use both -- preprocessor variables for static build-time values (like breakpoint definitions used in @media rules, which CSS custom properties cannot do) and CSS custom properties for anything that needs to be dynamic at runtime (like theming, user customization, and component variants).

Building a Design Token System

Design tokens are the foundational values of a design system -- colors, spacing, typography, shadows, and more. CSS custom properties are an ideal way to implement design tokens because they provide a centralized, maintainable, and dynamic token system directly in your CSS.

Example: A Complete Design Token System

:root {
    /* ===== Color Tokens ===== */
    /* Primitive colors (raw values) */
    --color-blue-50: #eff6ff;
    --color-blue-100: #dbeafe;
    --color-blue-500: #3b82f6;
    --color-blue-600: #2563eb;
    --color-blue-700: #1d4ed8;
    --color-gray-50: #f9fafb;
    --color-gray-100: #f3f4f6;
    --color-gray-200: #e5e7eb;
    --color-gray-500: #6b7280;
    --color-gray-700: #374151;
    --color-gray-900: #111827;
    --color-green-500: #22c55e;
    --color-red-500: #ef4444;
    --color-yellow-500: #eab308;

    /* Semantic colors (role-based references) */
    --color-primary: var(--color-blue-500);
    --color-primary-hover: var(--color-blue-600);
    --color-primary-active: var(--color-blue-700);
    --color-bg: var(--color-gray-50);
    --color-surface: #ffffff;
    --color-text: var(--color-gray-900);
    --color-text-muted: var(--color-gray-500);
    --color-border: var(--color-gray-200);
    --color-success: var(--color-green-500);
    --color-error: var(--color-red-500);
    --color-warning: var(--color-yellow-500);

    /* ===== Spacing Tokens ===== */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;
    --space-10: 40px;
    --space-12: 48px;
    --space-16: 64px;

    /* ===== Typography Tokens ===== */
    --font-sans: 'Inter', 'Segoe UI', sans-serif;
    --font-mono: 'Fira Code', 'Courier New', monospace;
    --text-xs: 0.75rem;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.25rem;
    --text-2xl: 1.5rem;
    --text-3xl: 1.875rem;
    --text-4xl: 2.25rem;
    --line-height-tight: 1.25;
    --line-height-normal: 1.5;
    --line-height-relaxed: 1.75;
    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-bold: 700;

    /* ===== Shadow Tokens ===== */
    --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
    --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);

    /* ===== Border Radius Tokens ===== */
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-xl: 16px;
    --radius-full: 9999px;

    /* ===== Transition Tokens ===== */
    --duration-fast: 150ms;
    --duration-normal: 300ms;
    --duration-slow: 500ms;
    --easing-default: cubic-bezier(0.4, 0, 0.2, 1);
}

Notice the two-tier approach: primitive tokens define raw values (like --color-blue-500), while semantic tokens reference the primitives and assign meaning (like --color-primary). This separation makes theming easier -- to create a new theme, you only change the semantic tokens to point to different primitives.

Practical Example: Dark/Light Theme Toggle

Let us build a complete dark/light theme toggle using everything we have covered. This example combines CSS custom properties with a small amount of JavaScript to create a fully functional theme switcher.

Example: Complete Theme Toggle Implementation

/* HTML structure */
<button id="theme-toggle" aria-label="Toggle dark mode">
    Toggle Theme
</button>

/* CSS */
:root {
    --color-bg: #ffffff;
    --color-surface: #f8f9fa;
    --color-text: #212529;
    --color-text-muted: #6c757d;
    --color-primary: #0d6efd;
    --color-border: #dee2e6;
    --color-code-bg: #f1f3f5;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --transition: 0.3s ease;
}

[data-theme="dark"] {
    --color-bg: #0d1117;
    --color-surface: #161b22;
    --color-text: #c9d1d9;
    --color-text-muted: #8b949e;
    --color-primary: #58a6ff;
    --color-border: #30363d;
    --color-code-bg: #1c2128;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

*, *::before, *::after {
    transition: background-color var(--transition),
                color var(--transition),
                border-color var(--transition),
                box-shadow var(--transition);
}

body {
    background-color: var(--color-bg);
    color: var(--color-text);
    font-family: system-ui, sans-serif;
    line-height: 1.6;
}

.card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    padding: 24px;
    box-shadow: var(--shadow);
}

code {
    background-color: var(--color-code-bg);
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 0.9em;
}

/* JavaScript */
const toggle = document.getElementById('theme-toggle');
const html = document.documentElement;

// Check for saved preference or system preference
const savedTheme = localStorage.getItem('theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

if (savedTheme) {
    html.setAttribute('data-theme', savedTheme);
} else if (systemDark) {
    html.setAttribute('data-theme', 'dark');
}

toggle.addEventListener('click', () => {
    const current = html.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';
    html.setAttribute('data-theme', next);
    localStorage.setItem('theme', next);
});

Practical Example: Component Variants

Custom properties make it easy to create component variants without duplicating styles. Here is a badge component with multiple color variants, all driven by a single set of variables.

Example: Badge Component with Variable-Driven Variants

.badge {
    --badge-bg: var(--color-gray-100);
    --badge-text: var(--color-gray-700);
    --badge-border: var(--color-gray-200);

    display: inline-flex;
    align-items: center;
    padding: 4px 12px;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    background-color: var(--badge-bg);
    color: var(--badge-text);
    border: 1px solid var(--badge-border);
    border-radius: var(--radius-full);
}

.badge-primary {
    --badge-bg: var(--color-blue-50);
    --badge-text: var(--color-blue-700);
    --badge-border: var(--color-blue-100);
}

.badge-success {
    --badge-bg: #dcfce7;
    --badge-text: #15803d;
    --badge-border: #bbf7d0;
}

.badge-danger {
    --badge-bg: #fef2f2;
    --badge-text: #b91c1c;
    --badge-border: #fecaca;
}

/* Size variants also use variables */
.badge-sm {
    --badge-font-size: var(--text-xs);
    padding: 2px 8px;
    font-size: var(--badge-font-size);
}

.badge-lg {
    --badge-font-size: var(--text-base);
    padding: 6px 16px;
    font-size: var(--badge-font-size);
}

Exercise 1: Build a Theme Customizer

Create a web page with a theme customizer panel that allows users to modify the design in real time. Start by defining a set of CSS custom properties on :root for your primary color, background color, text color, font size, and border radius. Build a simple page layout with a header, a card component, and a button. Then create a customizer panel with the following controls: a color input for the primary color, a range slider for font size (12px to 24px), a range slider for border radius (0px to 20px), and a toggle button for dark/light mode. Use JavaScript to read each input's value and update the corresponding CSS custom property using document.documentElement.style.setProperty(). Add a "Reset to Defaults" button that removes all inline style overrides. Save the user's preferences to localStorage and restore them on page load.

Exercise 2: Design Token System for a Card Component

Create a complete design token system and use it to build a card component with multiple variants. Define at least 15 custom properties on :root covering colors (primary, secondary, background, text, border), spacing (small, medium, large), typography (font family, base size, heading size), and shadows (small, medium). Build a .card component that uses local variables with fallbacks for its background, padding, border, shadow, and text color. Create at least four card variants: a default card, a highlighted card with a colored border, an elevated card with a larger shadow, and a compact card with reduced padding. Then add a dark theme by redefining the root variables under a [data-theme="dark"] selector, ensuring all four card variants adapt correctly. Finally, add a media query that adjusts the spacing variables for screens smaller than 768px.