Introduction to CSS Variables
CSS Custom Properties, commonly known as CSS Variables, provide a powerful way to create reusable values in your stylesheets. Unlike preprocessor variables (like those in Sass or Less), CSS Variables are:
- Native to CSS: They work directly in browsers without compilation
- Dynamic: Their values can be updated in real-time through JavaScript
- Cascade-aware: They follow the CSS cascade and can be scoped to specific elements
- Reactive: Changes propagate to all elements using the variable
CSS Variables are like design tokens or configuration values for your stylesheets. They allow you to define a value once and reference it throughout your CSS, making changes easier and more consistent.
The Syntax of CSS Variables
Defining Custom Properties
CSS Variables are defined using a double-hyphen prefix (--) followed by a name you choose:
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--text-color: #333333;
--spacing-unit: 8px;
--font-family-heading: 'Roboto', sans-serif;
--border-radius: 4px;
}
In this example, we're defining six variables within the :root selector, which makes them
available globally throughout the document. The :root selector targets the root element of
the document (typically the <html> element).
Using Custom Properties
To use a CSS Variable, you use the var() function:
.button {
background-color: var(--primary-color);
color: white;
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
border-radius: var(--border-radius);
font-family: var(--font-family-heading);
}
.card {
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: calc(var(--spacing-unit) * 2);
}
This is similar to how you might reference a constant in programming - you define the value once and then reference it by name wherever you need it.
Fallback Values
The var() function can accept a second parameter that serves as a fallback value if the
variable is not defined:
.element {
/* If --custom-width is not defined, 100% will be used */
width: var(--custom-width, 100%);
/* You can even use another variable as a fallback */
color: var(--text-color-secondary, var(--text-color-primary, #000));
}
This is like having a backup plan - if your first choice isn't available, you have alternatives ready.
Naming Conventions
While you can name variables however you want, consistent naming patterns make your code more maintainable:
:root {
/* Component-specific variables */
--btn-background: #3498db;
--btn-color: white;
/* Theme variables */
--theme-primary: #3498db;
--theme-secondary: #2ecc71;
/* Semantic variables */
--color-success: #2ecc71;
--color-warning: #f1c40f;
--color-error: #e74c3c;
/* Functional variables */
--spacing-small: 4px;
--spacing-medium: 8px;
--spacing-large: 16px;
}
Good naming conventions make your variables more discoverable and self-documenting, similar to how well-named functions and variables improve code readability in programming.
Scoping and the Cascade
One of the most powerful features of CSS Variables is their ability to follow the cascade and be scoped to specific elements. This allows you to override variables for specific components or states.
Global Variables
Variables defined in the :root selector are available throughout the document:
:root {
--primary-color: #3498db;
}
Local Variables
Variables can be defined within any selector, limiting their scope to that element and its descendants:
.card {
--card-padding: 16px;
padding: var(--card-padding);
}
.card__header {
/* This will use the --card-padding variable from the parent */
padding: var(--card-padding);
}
.sidebar {
/* This defines a different --card-padding for cards within the sidebar */
--card-padding: 8px;
}
This local scoping is similar to lexical scoping in programming languages - variables are accessible within their defined scope and any nested scopes.
Component Variations Through Scoping
/* Base button styles with default variables */
.button {
--button-bg: #3498db;
--button-color: white;
background-color: var(--button-bg);
color: var(--button-color);
padding: 8px 16px;
border: none;
border-radius: 4px;
}
/* Button variations through variable overrides */
.button--success {
--button-bg: #2ecc71;
}
.button--warning {
--button-bg: #f1c40f;
--button-color: #333;
}
.button--danger {
--button-bg: #e74c3c;
}
This approach allows you to create component variations without writing additional CSS properties, similar to how you might use configuration objects to customize instances of a class in object-oriented programming.
State-Based Variables
Variables can be changed based on state:
.accordion {
--icon-rotation: 0deg;
}
.accordion.is-open {
--icon-rotation: 180deg;
}
.accordion__icon {
transform: rotate(var(--icon-rotation));
transition: transform 0.3s ease;
}
This creates a clear relationship between the component's state and its visual appearance, similar to how state machines in programming define different visual states.
Practical Use Cases for CSS Variables
Theming and Dark Mode
One of the most powerful applications of CSS Variables is creating theme systems, including dark mode:
/* Define theme colors at the root level */
:root {
/* Light theme (default) */
--color-bg: #ffffff;
--color-text: #333333;
--color-primary: #3498db;
--color-secondary: #2ecc71;
--color-border: #e0e0e0;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Dark theme override */
.theme-dark {
--color-bg: #121212;
--color-text: #f5f5f5;
--color-primary: #90caf9;
--color-secondary: #81c784;
--color-border: #333333;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* Using the variables throughout the site */
body {
background-color: var(--color-bg);
color: var(--color-text);
}
.card {
background-color: var(--color-bg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow);
}
.button-primary {
background-color: var(--color-primary);
color: white;
}
This approach allows you to switch themes by simply adding a class to a container element, usually the body
or html element:
// JavaScript to toggle dark mode
const toggleTheme = () => {
document.body.classList.toggle('theme-dark');
}
You can also respect user preferences for dark mode:
/* Use dark theme when user prefers dark color scheme */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #121212;
--color-text: #f5f5f5;
--color-primary: #90caf9;
--color-secondary: #81c784;
--color-border: #333333;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
}
This reactive theming is like having a light switch for your website that instantly changes the entire ambiance without rebuilding anything.
Responsive Design Tokens
CSS Variables can adapt to different viewport sizes:
:root {
/* Base/mobile sizes */
--font-size-h1: 1.75rem;
--font-size-h2: 1.375rem;
--font-size-body: 1rem;
--spacing-unit: 8px;
--container-padding: 16px;
}
@media (min-width: 768px) {
:root {
/* Tablet sizes */
--font-size-h1: 2.25rem;
--font-size-h2: 1.75rem;
--spacing-unit: 12px;
--container-padding: 24px;
}
}
@media (min-width: 1200px) {
:root {
/* Desktop sizes */
--font-size-h1: 2.75rem;
--font-size-h2: 2rem;
--spacing-unit: 16px;
--container-padding: 32px;
}
}
/* Using the responsive variables */
h1 {
font-size: var(--font-size-h1);
margin-bottom: calc(var(--spacing-unit) * 2);
}
.container {
padding: var(--container-padding);
}
This approach centralizes your responsive adjustments, making it easier to maintain consistent scaling across your site. It's like having a master control panel for all your responsive design decisions.
Component Variants
CSS Variables make it easy to create component variants:
/* Base alert component with default variables */
.alert {
--alert-color: #3498db;
--alert-bg: #e3f2fd;
--alert-border: #bbdefb;
color: var(--alert-color);
background-color: var(--alert-bg);
border: 1px solid var(--alert-border);
border-radius: 4px;
padding: 12px 16px;
}
/* Alert variants through variable overrides */
.alert--success {
--alert-color: #2e7d32;
--alert-bg: #e8f5e9;
--alert-border: #c8e6c9;
}
.alert--warning {
--alert-color: #f57f17;
--alert-bg: #fff8e1;
--alert-border: #ffecb3;
}
.alert--error {
--alert-color: #c62828;
--alert-bg: #ffebee;
--alert-border: #ffcdd2;
}
This approach reduces code duplication and makes relationships between variants explicit. It's like having a base recipe with variations that only specify what changes.
Calculated Values
CSS Variables can be used in calculations with calc():
:root {
--spacing-unit: 8px;
--header-height: 60px;
--sidebar-width: 250px;
}
.header {
height: var(--header-height);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
}
.sidebar {
width: var(--sidebar-width);
}
.main-content {
margin-left: var(--sidebar-width);
min-height: calc(100vh - var(--header-height));
padding: calc(var(--spacing-unit) * 3);
}
/* Grid system based on spacing unit */
.grid {
display: grid;
grid-gap: var(--spacing-unit);
grid-template-columns: repeat(auto-fill, minmax(calc(var(--spacing-unit) * 30), 1fr));
}
This allows you to create derived values that automatically update when the base value changes, similar to formulas in a spreadsheet that recalculate when input values change.
Manipulating CSS Variables with JavaScript
One of the most powerful aspects of CSS Variables is their ability to be manipulated with JavaScript, allowing for dynamic styling without directly modifying individual CSS properties.
Reading CSS Variable Values
// Get the value of a CSS variable
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color').trim();
console.log(primaryColor); // e.g., "#3498db"
Setting CSS Variable Values
// Set a CSS variable on the root element (global)
document.documentElement.style.setProperty('--primary-color', '#ff0000');
// Set a CSS variable on a specific element (local)
const header = document.querySelector('.header');
header.style.setProperty('--header-bg', '#000000');
Interactive User Preferences
CSS Variables can be used to create interactive user preference systems:
<!-- HTML for a color picker -->
<label for="theme-color">Theme Color:</label>
<input type="color" id="theme-color" value="#3498db">
<script>
// Update theme color when the input changes
const colorPicker = document.getElementById('theme-color');
colorPicker.addEventListener('input', (e) => {
// Set the primary color variable
document.documentElement.style.setProperty('--primary-color', e.target.value);
// Calculate and set secondary colors based on the primary color
const primaryHsl = hexToHSL(e.target.value);
// Create a darker variant for hover states
const darkerColor = `hsl(${primaryHsl.h}, ${primaryHsl.s}%, ${primaryHsl.l - 10}%)`;
document.documentElement.style.setProperty('--primary-color-dark', darkerColor);
// Create a lighter variant for backgrounds
const lighterColor = `hsl(${primaryHsl.h}, ${primaryHsl.s}%, 95%)`;
document.documentElement.style.setProperty('--primary-color-light', lighterColor);
});
// Helper function to convert hex to HSL
function hexToHSL(hex) {
// Conversion logic here...
return { h: 210, s: 70, l: 50 }; // Example return
}
</script>
This creates a system where users can customize the appearance of the site while maintaining design coherence, similar to how software applications often provide theme customization options.
Real-time Animation and Interaction
CSS Variables can be used to create dynamic animations based on user interaction:
.cursor-follower {
--mouse-x: 0;
--mouse-y: 0;
position: fixed;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: var(--primary-color);
transform: translate(calc(var(--mouse-x) - 50%), calc(var(--mouse-y) - 50%));
pointer-events: none;
opacity: 0.7;
transition: transform 0.1s ease-out;
}
<script>
const follower = document.querySelector('.cursor-follower');
document.addEventListener('mousemove', (e) => {
// Update CSS variables with current mouse position
follower.style.setProperty('--mouse-x', `${e.clientX}px`);
follower.style.setProperty('--mouse-y', `${e.clientY}px`);
});
</script>
This approach separates the interaction logic (tracking mouse position) from the visual rendering (using CSS transforms), creating a clean separation of concerns like the Model-View pattern in software architecture.
Dynamic Layout Control
CSS Variables can be used to control layout dynamically:
<!-- HTML Slider for controlling layout -->
<label for="columns-slider">Columns: <span id="columns-value">3</span></label>
<input type="range" id="columns-slider" min="1" max="6" value="3">
<div class="grid">
<!-- Grid items -->
</div>
<style>
:root {
--grid-columns: 3;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
grid-gap: 16px;
}
</style>
<script>
const slider = document.getElementById('columns-slider');
const columnsValue = document.getElementById('columns-value');
slider.addEventListener('input', (e) => {
const columns = e.target.value;
columnsValue.textContent = columns;
document.documentElement.style.setProperty('--grid-columns', columns);
});
</script>
This pattern allows for interactive, user-controlled layouts without requiring complex JavaScript to modify individual element styles.
CSS Variables vs. Preprocessor Variables
While CSS Variables and preprocessor variables (like those in Sass, Less, or Stylus) serve similar purposes, they have key differences that make them suitable for different scenarios.
| Feature | CSS Variables | Preprocessor Variables |
|---|---|---|
| Browser Support | Requires modern browsers (IE11 not fully supported) | Compiled to standard CSS, works everywhere |
| Runtime Changes | Can be changed with JavaScript at runtime | Static, determined at compile time |
| Cascade & Inheritance | Follows CSS cascade, can be scoped to elements | Global by default in scope of file/module |
| Computed Values | Values can be computed by the browser | Values are computed during compilation |
| Syntax | --name: value; and var(--name) |
$name: value; and $name (Sass) |
| Conditionals & Functions | Limited to what CSS supports | Rich programming features like conditionals, loops, functions |
When to Use CSS Variables
- When you need to change values dynamically with JavaScript
- For theming and user customization
- When you need to respond to media queries
- When you want to scope variables to components
When to Use Preprocessor Variables
- When you need advanced logic (conditionals, loops, functions)
- For compilation-time calculations
- When you need to support older browsers like IE11
- For global configuration that doesn't change at runtime
Using Both Together
Many modern projects use both types of variables for their respective strengths:
// Sass file with both types of variables
// Preprocessor variables for build-time configuration
$breakpoint-sm: 576px;
$breakpoint-md: 768px;
$breakpoint-lg: 992px;
$breakpoint-xl: 1200px;
// Generate CSS Variables for runtime use
:root {
// Base colors
--color-primary: #3498db;
--color-secondary: #2ecc71;
// Base sizes
--spacing-unit: 8px;
--font-size-base: 16px;
// Generate derived variables from preprocessor variables
@media (min-width: $breakpoint-md) {
--spacing-unit: 12px;
--font-size-base: 18px;
}
@media (min-width: $breakpoint-lg) {
--spacing-unit: 16px;
}
}
// Using preprocessor variables for grid generation
.container {
max-width: $breakpoint-lg;
margin: 0 auto;
@media (min-width: $breakpoint-xl) {
max-width: $breakpoint-xl;
}
}
// Using CSS Variables for component styling
.button {
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
background-color: var(--color-primary);
font-size: var(--font-size-base);
}
This approach gives you the best of both worlds - preprocessor variables for static configuration and complex logic, and CSS Variables for runtime theming and responsive adjustments.
Design Tokens and Design Systems
CSS Variables are a perfect fit for implementing design tokens in a design system. Design tokens are named entities that store visual design attributes, creating a bridge between design tools and code.
Creating a Design Token System
:root {
/* Color Tokens */
--color-brand-primary: #0066cc;
--color-brand-secondary: #ff9900;
--color-neutral-100: #ffffff;
--color-neutral-200: #f8f9fa;
--color-neutral-300: #e9ecef;
--color-neutral-400: #dee2e6;
--color-neutral-500: #adb5bd;
--color-neutral-600: #6c757d;
--color-neutral-700: #495057;
--color-neutral-800: #343a40;
--color-neutral-900: #212529;
--color-feedback-success: #28a745;
--color-feedback-warning: #ffc107;
--color-feedback-error: #dc3545;
--color-feedback-info: #17a2b8;
/* Typography Tokens */
--font-family-base: 'Inter', system-ui, sans-serif;
--font-family-heading: 'Montserrat', system-ui, sans-serif;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-md: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
--font-size-4xl: 2.25rem; /* 36px */
/* Spacing Tokens */
--spacing-xs: 0.25rem; /* 4px */
--spacing-sm: 0.5rem; /* 8px */
--spacing-md: 1rem; /* 16px */
--spacing-lg: 1.5rem; /* 24px */
--spacing-xl: 2rem; /* 32px */
--spacing-2xl: 3rem; /* 48px */
--spacing-3xl: 4rem; /* 64px */
/* Border Tokens */
--border-radius-sm: 0.125rem; /* 2px */
--border-radius-md: 0.25rem; /* 4px */
--border-radius-lg: 0.5rem; /* 8px */
--border-radius-full: 9999px;
--border-width-thin: 1px;
--border-width-thick: 2px;
/* Shadow Tokens */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* Animation Tokens */
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
--easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
--easing-accelerate: cubic-bezier(0.4, 0, 1, 1);
--easing-decelerate: cubic-bezier(0, 0, 0.2, 1);
}
Semantic Aliases
To make the design system more maintainable, you can create semantic aliases that reference the base tokens:
:root {
/* Base tokens defined here... */
/* Semantic Aliases */
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-700);
--color-text-tertiary: var(--color-neutral-600);
--color-text-inverse: var(--color-neutral-100);
--color-background-default: var(--color-neutral-100);
--color-background-subtle: var(--color-neutral-200);
--color-background-accent: var(--color-brand-primary);
--color-border-default: var(--color-neutral-300);
--color-border-strong: var(--color-neutral-400);
--font-body: var(--font-size-md)/1.5 var(--font-family-base);
--font-heading-1: var(--font-weight-bold) var(--font-size-4xl)/1.2 var(--font-family-heading);
--font-heading-2: var(--font-weight-bold) var(--font-size-3xl)/1.2 var(--font-family-heading);
--font-heading-3: var(--font-weight-bold) var(--font-size-2xl)/1.3 var(--font-family-heading);
--font-caption: var(--font-size-sm)/1.4 var(--font-family-base);
--spacing-component-sm: var(--spacing-sm);
--spacing-component-md: var(--spacing-md);
--spacing-component-lg: var(--spacing-lg);
--spacing-layout-sm: var(--spacing-lg);
--spacing-layout-md: var(--spacing-xl);
--spacing-layout-lg: var(--spacing-2xl);
}
This two-level system creates a separation between the raw values (which rarely change) and their semantic usage in the UI (which might change more frequently). It's like having fundamental elements in chemistry that combine to form different compounds with specific purposes.
Component-Specific Tokens
Components can define their own variables that reference the global tokens:
/* Button Component Tokens */
.button {
--button-padding-x: var(--spacing-md);
--button-padding-y: var(--spacing-sm);
--button-font-size: var(--font-size-md);
--button-border-radius: var(--border-radius-md);
--button-transition: background-color var(--duration-fast) var(--easing-standard);
/* Default variant */
--button-bg: var(--color-brand-primary);
--button-color: var(--color-neutral-100);
--button-border-color: var(--color-brand-primary);
--button-hover-bg: #0055aa; /* Darker variant of primary */
/* Apply component tokens to the element */
padding: var(--button-padding-y) var(--button-padding-x);
font-size: var(--button-font-size);
background-color: var(--button-bg);
color: var(--button-color);
border: var(--border-width-thin) solid var(--button-border-color);
border-radius: var(--button-border-radius);
transition: var(--button-transition);
}
.button:hover {
background-color: var(--button-hover-bg);
}
/* Button Variants */
.button--secondary {
--button-bg: transparent;
--button-color: var(--color-brand-primary);
--button-border-color: var(--color-brand-primary);
--button-hover-bg: rgba(0, 102, 204, 0.1); /* Semi-transparent primary */
}
.button--tertiary {
--button-bg: transparent;
--button-color: var(--color-text-primary);
--button-border-color: transparent;
--button-hover-bg: var(--color-neutral-200);
}
This approach creates a cohesive design system where components share common design tokens but can be customized as needed. It's similar to how a design system like Material Design has global principles but allows customization for specific applications.
Browser Support and Fallbacks
CSS Variables are well-supported in modern browsers, but older browsers (particularly IE11) do not support them or have limited support. Here are strategies for providing fallbacks:
Feature Detection Approach
/* Fallback for browsers that don't support CSS Variables */
.button {
background-color: #3498db; /* Fallback color */
background-color: var(--primary-color, #3498db); /* Modern browsers will use this */
}
/* Using @supports for feature detection */
@supports (--css: variables) {
/* CSS that only runs in browsers that support variables */
.theme-dark {
--primary-color: #90caf9;
}
}
Modernizr Approach
<!-- Include Modernizr for feature detection -->
<script src="modernizr.js"></script>
<style>
/* Fallback styles */
.button {
background-color: #3498db;
}
/* Styles for browsers that support CSS Variables */
.cssvarssupport .button {
background-color: var(--primary-color, #3498db);
}
</style>
<script>
// Modernizr adds classes to the HTML element
// If CSS Variables are supported, the class 'cssvarssupport' is added
</script>
PostCSS Approach
For a more automated solution, you can use PostCSS with the postcss-custom-properties plugin to generate static fallbacks during build:
/* Your source CSS with variables */
:root {
--primary-color: #3498db;
}
.button {
background-color: var(--primary-color);
}
/* After PostCSS processing, becomes: */
:root {
--primary-color: #3498db;
}
.button {
background-color: #3498db; /* Static fallback */
background-color: var(--primary-color); /* For modern browsers */
}
This approach automatically generates fallbacks while still preserving the dynamic nature of CSS Variables in modern browsers.
Progressive Enhancement
A general approach is to treat CSS Variables as progressive enhancement:
- Design your site to work with static values first
- Add CSS Variables to enhance the experience in modern browsers
- Use dynamic features like theming as an enhancement, not a core requirement
This ensures your site works for all users, with an enhanced experience for those with modern browsers.
Best Practices for CSS Variables
Naming Conventions
-
Be descriptive: Use names that clearly indicate the purpose of the variable
/* Good */ --color-primary: #3498db; --spacing-unit: 8px; --font-heading: 'Roboto', sans-serif; /* Avoid */ --c1: #3498db; --s1: 8px; --f1: 'Roboto', sans-serif; -
Use a consistent naming pattern: Adopt a naming scheme like category-property-variant
--color-background-primary: #ffffff; --color-background-secondary: #f8f8f8; --spacing-margin-small: 8px; --spacing-margin-medium: 16px; -
Namespace component-specific variables: Prefix variables with component names
.button { --button-background: var(--color-primary); --button-padding: var(--spacing-unit); }
Organization
-
Group related variables: Keep similar variables together
:root { /* Colors */ --color-primary: #3498db; --color-secondary: #2ecc71; /* Typography */ --font-family-base: 'Open Sans', sans-serif; --font-size-base: 16px; /* Spacing */ --spacing-unit: 8px; --spacing-large: 16px; } -
Document your variables: Add comments to explain the purpose or usage of variables
:root { /* Primary brand color, used for main CTA buttons and primary actions */ --color-brand-primary: #0066cc; /* Spacing unit - all spacing should be multiples of this */ --spacing-unit: 8px; } -
Create logical layers: Organize variables from low-level tokens to high-level semantics
:root { /* Base Tokens - Fundamental values */ --color-blue-500: #3498db; --color-green-500: #2ecc71; /* Semantic Tokens - Purpose-based values */ --color-primary: var(--color-blue-500); --color-success: var(--color-green-500); /* Component Tokens - Used within specific contexts */ --button-background: var(--color-primary); --alert-success-background: var(--color-success); }
Performance Considerations
- Limit the scope: Define variables at the most specific level needed to avoid unnecessary inheritance
- Be cautious with JavaScript updates: Changing variables triggers recalculation and painting, so batch updates when possible
- Avoid deep dependency chains: Having variables that depend on other variables that depend on other variables can get complex and harder to debug
Maintainability Tips
-
Create a variables stylesheet: Keep all global variables in a dedicated file for easy reference
/* variables.css */ :root { /* Global variables defined here */ } /* main.css */ @import 'variables.css'; /* component.css */ @import 'variables.css'; - Use CSS variables as an API: Think of variables as the public interface for your styles, with actual properties as implementation details
- Document your variable system: Create a style guide that explains your variable naming scheme and usage patterns
Practice Activity: Design Token System
Activity Instructions:
-
Create a design token system using CSS Variables for a hypothetical app or website. Your system should include:
- Base tokens for colors, typography, spacing, and other fundamental values
- Semantic tokens that reference the base tokens
- Component-specific tokens for at least three components
- Implement the components using your token system
- Create a light and dark theme that switches by changing variables, not by redefining components
- Add a JavaScript feature that allows users to customize at least one aspect of your design (like primary color)
Starter Code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design Token System</title>
<style>
/* Base Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
line-height: 1.5;
}
/* Your Design Token System Goes Here */
:root {
/* Base Tokens */
/* Semantic Tokens */
/* Component Tokens */
}
/* Dark Theme */
.theme-dark {
/* Override relevant variables for dark theme */
}
/* Components */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
margin-bottom: 20px;
}
.theme-toggle {
margin-bottom: 20px;
}
.color-picker {
margin-bottom: 20px;
}
.component-demo {
margin-bottom: 40px;
}
/* Component 1: Card */
.card {
/* Use component-specific tokens */
}
/* Component 2: Button */
.button {
/* Use component-specific tokens */
}
/* Component 3: Alert */
.alert {
/* Use component-specific tokens */
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Design Token System</h1>
<p>A demonstration of a design system using CSS Variables</p>
</header>
<div class="theme-toggle">
<button id="theme-toggle-btn">Toggle Dark Mode</button>
</div>
<div class="color-picker">
<label for="primary-color">Primary Color: </label>
<input type="color" id="primary-color" value="#3498db">
</div>
<section class="component-demo">
<h2>Cards</h2>
<div class="card">
<h3>Card Title</h3>
<p>This is a card component that uses our design tokens for consistent styling.</p>
<button class="button">Read More</button>
</div>
</section>
<section class="component-demo">
<h2>Buttons</h2>
<button class="button">Primary Button</button>
<button class="button button--secondary">Secondary Button</button>
<button class="button button--tertiary">Tertiary Button</button>
</section>
<section class="component-demo">
<h2>Alerts</h2>
<div class="alert alert--info">This is an informational alert.</div>
<div class="alert alert--success">This is a success alert.</div>
<div class="alert alert--warning">This is a warning alert.</div>
<div class="alert alert--error">This is an error alert.</div>
</section>
</div>
<script>
// Theme toggle functionality
const themeToggleBtn = document.getElementById('theme-toggle-btn');
themeToggleBtn.addEventListener('click', () => {
document.body.classList.toggle('theme-dark');
});
// Color picker functionality
const colorPicker = document.getElementById('primary-color');
colorPicker.addEventListener('input', (e) => {
// Update primary color variable
// Your code here
});
</script>
</body>
</html>
Extra Challenge:
Extend your system with:
- Responsive tokens that change based on viewport size
- A user preferences form that allows adjusting multiple aspects of the UI (spacing, font size, etc.)
- Save user preferences to localStorage so they persist between visits
Conclusion
CSS Custom Properties (CSS Variables) have transformed how we write and organize CSS, offering a powerful way to create more maintainable, dynamic, and flexible stylesheets. By leveraging the cascade, scope, and JavaScript integration of CSS Variables, we can build sophisticated design systems that adapt to user preferences, viewport sizes, and interaction states.
As you continue to develop your CSS skills, consider incorporating CSS Variables into your workflow, particularly for:
- Creating cohesive design token systems
- Implementing theme switching and dark mode
- Building responsive designs with centralized breakpoint management
- Enabling user customization options
- Crafting component-based architectures with clear style interfaces
In our next lecture, we'll explore CSS Modules and Component-Based Styling, which builds on these concepts to create even more robust approaches to styling modern web applications.