CSS Variables
CSS Custom Properties, commonly known as CSS variables, are entities defined by authors that contain specific values to be reused throughout a document. Unlike preprocessor variables (Sass, Less), CSS variables are live — they can be changed at runtime via JavaScript, respond to the cascade, and inherit through the DOM tree.
CSS variables enable powerful patterns that were previously impossible: dynamic theming without duplicate stylesheets, component-scoped customization, responsive property changes, and real-time style updates from JavaScript.
| 1 | :root { |
| 2 | --accent: #00FF41; |
| 3 | --bg: #0D0D0D; |
| 4 | --text: #E0E0E0; |
| 5 | --muted: #808080; |
| 6 | } |
| 7 | |
| 8 | .element { |
| 9 | color: var(--accent); |
| 10 | background: var(--bg); |
| 11 | } |
Custom properties use a -- prefix followed by a name (case-sensitive). They are scoped to the element they are defined on, plus its descendants. The :root pseudo-class is used for global variables.
| 1 | /* Global variables on :root */ |
| 2 | :root { |
| 3 | --color-primary: #00FF41; |
| 4 | --color-bg: #0D0D0D; |
| 5 | --color-text: #E0E0E0; |
| 6 | --color-muted: #808080; |
| 7 | --color-border: #222222; |
| 8 | --font-mono: "SF Mono", Menlo, monospace; |
| 9 | --font-sans: system-ui, -apple-system, sans-serif; |
| 10 | --spacing-xs: 4px; |
| 11 | --spacing-sm: 8px; |
| 12 | --spacing-md: 16px; |
| 13 | --spacing-lg: 24px; |
| 14 | --spacing-xl: 32px; |
| 15 | --radius-sm: 4px; |
| 16 | --radius-md: 8px; |
| 17 | --radius-lg: 12px; |
| 18 | --shadow-sm: 0 1px 3px rgba(0,0,0,0.12); |
| 19 | --shadow-md: 0 4px 12px rgba(0,0,0,0.15); |
| 20 | --transition-fast: 150ms ease; |
| 21 | --transition-normal: 250ms ease; |
| 22 | } |
| 23 | |
| 24 | /* Scoped component variables */ |
| 25 | .card { |
| 26 | --card-padding: 20px; |
| 27 | --card-bg: #1A1A2E; |
| 28 | |
| 29 | background: var(--card-bg); |
| 30 | padding: var(--card-padding); |
| 31 | border-radius: var(--radius-md); |
| 32 | } |
| 33 | |
| 34 | /* Variables hold any CSS value */ |
| 35 | :root { |
| 36 | --gradient: linear-gradient(135deg, #00FF41, transparent); |
| 37 | --border: 2px solid var(--color-primary); |
| 38 | } |
info
The var() function retrieves a custom property value. It accepts the property name and an optional fallback. If the property is not defined, the fallback is used.
| 1 | /* Basic usage */ |
| 2 | .element { |
| 3 | color: var(--color-primary); |
| 4 | background: var(--color-bg); |
| 5 | } |
| 6 | |
| 7 | /* With fallback */ |
| 8 | .element { |
| 9 | color: var(--color-primary, #00FF41); |
| 10 | background: var(--color-bg, #0D0D0D); |
| 11 | } |
| 12 | |
| 13 | /* Nested fallbacks */ |
| 14 | .element { |
| 15 | padding: var(--spacing-xl, var(--spacing-lg, 32px)); |
| 16 | } |
| 17 | |
| 18 | /* In shorthand properties */ |
| 19 | .element { |
| 20 | border: var(--border-width, 2px) solid var(--color-primary, #00FF41); |
| 21 | } |
| 22 | |
| 23 | /* In calc() */ |
| 24 | .element { |
| 25 | padding: calc(var(--spacing-md) * 2); |
| 26 | width: calc(100% - var(--sidebar-width, 250px)); |
| 27 | } |
Providing robust fallbacks ensures graceful degradation when custom properties are undefined or when the browser does not support them.
| 1 | /* Direct fallback */ |
| 2 | .element { |
| 3 | color: var(--undefined-var, #00FF41); |
| 4 | } |
| 5 | |
| 6 | /* Fallback chain */ |
| 7 | .element { |
| 8 | padding: var(--spacing-xl, var(--spacing-lg, var(--spacing-md, 32px))); |
| 9 | } |
| 10 | |
| 11 | /* Fallback for unsupported browsers */ |
| 12 | .element { |
| 13 | color: #00FF41; /* older browsers */ |
| 14 | color: var(--color-primary); /* modern override */ |
| 15 | } |
| 16 | |
| 17 | /* Practical terminal fallback */ |
| 18 | .terminal-element { |
| 19 | background: #0D0D0D; |
| 20 | background: var(--terminal-bg, #0D0D0D); |
| 21 | color: #E0E0E0; |
| 22 | color: var(--terminal-text, #E0E0E0); |
| 23 | } |
warning
CSS variables inherit through the DOM tree. A variable on a parent is accessible to all children. Overriding on a child creates a new scope for that subtree. This inheritance crosses shadow DOM boundaries, making variables ideal for web component theming.
| 1 | /* Global scope */ |
| 2 | :root { |
| 3 | --color: #00FF41; |
| 4 | --bg: #0D0D0D; |
| 5 | } |
| 6 | |
| 7 | /* Override in section */ |
| 8 | .sidebar { |
| 9 | --color: #FFB000; |
| 10 | --bg: #1A1A1A; |
| 11 | } |
| 12 | |
| 13 | .sidebar .item { |
| 14 | color: var(--color); /* #FFB000 — from .sidebar */ |
| 15 | background: var(--bg); /* #1A1A1A */ |
| 16 | } |
| 17 | |
| 18 | /* Per-element override */ |
| 19 | .item.special { |
| 20 | --color: #3B82F6; |
| 21 | } |
| 22 | |
| 23 | /* Shadow DOM theming */ |
| 24 | /* Custom properties penetrate shadow DOM automatically */ |
CSS variables make dynamic theming trivial. Define color schemes as variable sets and swap a class or attribute on a parent element — the entire UI updates instantly without repaints or re-layouts.
| 1 | /* Light theme (default) */ |
| 2 | :root { |
| 3 | --bg-primary: #FFFFFF; |
| 4 | --bg-secondary: #F5F5F5; |
| 5 | --text-primary: #1A1A1A; |
| 6 | --text-secondary: #666666; |
| 7 | --accent: #00FF41; |
| 8 | --border: #E0E0E0; |
| 9 | } |
| 10 | |
| 11 | /* Dark theme */ |
| 12 | [data-theme="dark"] { |
| 13 | --bg-primary: #0D0D0D; |
| 14 | --bg-secondary: #1A1A2E; |
| 15 | --text-primary: #E0E0E0; |
| 16 | --text-secondary: #808080; |
| 17 | --accent: #00FF41; |
| 18 | --border: #222222; |
| 19 | } |
| 20 | |
| 21 | /* Terminal theme */ |
| 22 | [data-theme="terminal"] { |
| 23 | --bg-primary: #0D0D0D; |
| 24 | --text-primary: #E0E0E0; |
| 25 | --accent: #00FF41; |
| 26 | --font-family: "SF Mono", Menlo, monospace; |
| 27 | } |
| 28 | |
| 29 | /* Use variables */ |
| 30 | body { |
| 31 | background: var(--bg-primary); |
| 32 | color: var(--text-primary); |
| 33 | font-family: var(--font-family, system-ui); |
| 34 | } |
| 35 | |
| 36 | .card { |
| 37 | background: var(--bg-secondary); |
| 38 | border: 1px solid var(--border); |
| 39 | } |
| 40 | |
| 41 | /* Toggle via class on html */ |
| 42 | html.dark { |
| 43 | --bg-primary: #0D0D0D; |
| 44 | --text-primary: #E0E0E0; |
| 45 | } |
CSS custom properties can be read and modified at runtime using the CSS Object Model API. This enables dynamic theming, real-time adjustments, and user-driven customization.
| 1 | // Set a variable on an element |
| 2 | element.style.setProperty("--color-primary", "#00FF41"); |
| 3 | |
| 4 | // Set global variable on :root |
| 5 | document.documentElement.style.setProperty("--theme", "dark"); |
| 6 | |
| 7 | // Read a variable value |
| 8 | const color = getComputedStyle(element) |
| 9 | .getPropertyValue("--color-primary") |
| 10 | .trim(); |
| 11 | |
| 12 | // Toggle theme |
| 13 | function toggleTheme() { |
| 14 | const root = document.documentElement; |
| 15 | root.style.setProperty("--bg-primary", |
| 16 | root.style.getPropertyValue("--bg-primary") === "#0D0D0D" |
| 17 | ? "#FFFFFF" : "#0D0D0D" |
| 18 | ); |
| 19 | root.style.setProperty("--text-primary", |
| 20 | root.style.getPropertyValue("--text-primary") === "#E0E0E0" |
| 21 | ? "#1A1A1A" : "#E0E0E0" |
| 22 | ); |
| 23 | } |
| 24 | |
| 25 | // Responsive variables |
| 26 | window.addEventListener("resize", () => { |
| 27 | const vw = window.innerWidth; |
| 28 | document.documentElement.style.setProperty( |
| 29 | "--responsive-padding", |
| 30 | vw < 768 ? "16px" : "32px" |
| 31 | ); |
| 32 | }); |
| 1 | <!-- Theme toggle with persistence --> |
| 2 | <button onclick="setTheme('light')">Light</button> |
| 3 | <button onclick="setTheme('dark')">Dark</button> |
| 4 | <button onclick="setTheme('terminal')">Terminal</button> |
| 5 | |
| 6 | <script> |
| 7 | function setTheme(theme) { |
| 8 | document.documentElement.setAttribute("data-theme", theme); |
| 9 | localStorage.setItem("theme", theme); |
| 10 | } |
| 11 | const saved = localStorage.getItem("theme"); |
| 12 | if (saved) document.documentElement.setAttribute("data-theme", saved); |
| 13 | </script> |
pro tip
The @property at-rule registers custom properties with a defined syntax, initial value, and inheritance behavior. Typed variables enable browser animation of custom properties and value validation — impossible with regular var().
| 1 | /* Register typed custom properties */ |
| 2 | @property --brand-color { |
| 3 | syntax: "<color>"; |
| 4 | inherits: false; |
| 5 | initial-value: #00FF41; |
| 6 | } |
| 7 | |
| 8 | @property --spacing { |
| 9 | syntax: "<length>"; |
| 10 | inherits: true; |
| 11 | initial-value: 16px; |
| 12 | } |
| 13 | |
| 14 | @property --rotation { |
| 15 | syntax: "<angle>"; |
| 16 | inherits: false; |
| 17 | initial-value: 0deg; |
| 18 | } |
| 19 | |
| 20 | /* Now CSS can animate custom properties */ |
| 21 | @keyframes spin { |
| 22 | from { --rotation: 0deg; } |
| 23 | to { --rotation: 360deg; } |
| 24 | } |
| 25 | |
| 26 | .animated { |
| 27 | --rotation: 0deg; |
| 28 | transform: rotate(var(--rotation)); |
| 29 | animation: spin 2s linear infinite; |
| 30 | } |
| 31 | |
| 32 | /* Supported syntax types */ |
| 33 | /* <length>, <number>, <percentage>, <color> */ |
| 34 | /* <image>, <url>, <integer>, <angle> */ |
| 35 | /* <time>, <resolution>, <custom-ident> */ |
| 36 | /* <transform-function>, <transform-list> */ |
| 37 | /* * — any valid value */ |
best practice
A complete terminal design token system using CSS variables. This pattern powers this documentation site and can be the foundation for any terminal-styled application.
| 1 | /* Terminal Design Token System */ |
| 2 | :root { |
| 3 | /* Core palette */ |
| 4 | --terminal-bg: #0D0D0D; |
| 5 | --terminal-surface: #0A0A0A; |
| 6 | --terminal-elevated: #1A1A2E; |
| 7 | |
| 8 | /* Text hierarchy */ |
| 9 | --terminal-text-primary: #E0E0E0; |
| 10 | --terminal-text-body: #A0A0A0; |
| 11 | --terminal-text-muted: #808080; |
| 12 | --terminal-text-dim: #525252; |
| 13 | |
| 14 | /* Accent colors */ |
| 15 | --terminal-accent: #00FF41; |
| 16 | --terminal-accent-dim: rgba(0, 255, 65, 0.15); |
| 17 | --terminal-warning: #FFB000; |
| 18 | --terminal-error: #EF4444; |
| 19 | --terminal-info: #3B82F6; |
| 20 | |
| 21 | /* Borders */ |
| 22 | --terminal-border: #222222; |
| 23 | --terminal-border-light: rgba(0, 255, 65, 0.1); |
| 24 | |
| 25 | /* Typography */ |
| 26 | --terminal-font-mono: "SF Mono", Menlo, "Fira Code", monospace; |
| 27 | --terminal-font-ui: system-ui, -apple-system, sans-serif; |
| 28 | --terminal-font-size: 13px; |
| 29 | --terminal-line-height: 1.5; |
| 30 | |
| 31 | /* Spacing */ |
| 32 | --terminal-space-xs: 4px; |
| 33 | --terminal-space-sm: 8px; |
| 34 | --terminal-space-md: 16px; |
| 35 | --terminal-space-lg: 24px; |
| 36 | --terminal-space-xl: 32px; |
| 37 | |
| 38 | /* Effects */ |
| 39 | --terminal-radius: 8px; |
| 40 | --terminal-glow: 0 0 20px rgba(0, 255, 65, 0.15); |
| 41 | --terminal-transition: 200ms ease; |
| 42 | } |
| 43 | |
| 44 | /* Usage */ |
| 45 | .terminal-panel { |
| 46 | background: var(--terminal-surface); |
| 47 | border: 1px solid var(--terminal-border); |
| 48 | border-radius: var(--terminal-radius); |
| 49 | padding: var(--terminal-space-md); |
| 50 | color: var(--terminal-text-body); |
| 51 | font-family: var(--terminal-font-mono); |
| 52 | font-size: var(--terminal-font-size); |
| 53 | line-height: var(--terminal-line-height); |
| 54 | } |
| 55 | |
| 56 | .terminal-accent { |
| 57 | color: var(--terminal-accent); |
| 58 | } |
| 59 | |
| 60 | .terminal-dim { |
| 61 | color: var(--terminal-text-dim); |
| 62 | } |
best practice