Back to Blog
CSSThemingCustom PropertiesDark Mode

CSS Custom Properties for Theming

Build flexible themes with CSS variables. From basic usage to dark mode to dynamic theming patterns.

B
Bootspring Team
Engineering
April 10, 2022
5 min read

CSS custom properties enable powerful theming systems. Here's how to build flexible, maintainable themes.

Basic Custom Properties#

1/* Define variables in :root for global scope */ 2:root { 3 --color-primary: #3b82f6; 4 --color-secondary: #64748b; 5 --color-success: #22c55e; 6 --color-error: #ef4444; 7 8 --font-sans: system-ui, -apple-system, sans-serif; 9 --font-mono: 'Fira Code', monospace; 10 11 --spacing-xs: 0.25rem; 12 --spacing-sm: 0.5rem; 13 --spacing-md: 1rem; 14 --spacing-lg: 2rem; 15 --spacing-xl: 4rem; 16 17 --radius-sm: 0.25rem; 18 --radius-md: 0.5rem; 19 --radius-lg: 1rem; 20 --radius-full: 9999px; 21} 22 23/* Use variables */ 24.button { 25 background-color: var(--color-primary); 26 padding: var(--spacing-sm) var(--spacing-md); 27 border-radius: var(--radius-md); 28 font-family: var(--font-sans); 29} 30 31/* Fallback values */ 32.card { 33 background: var(--color-surface, #ffffff); 34}

Dark Mode#

1/* Light theme (default) */ 2:root { 3 --color-bg: #ffffff; 4 --color-surface: #f8fafc; 5 --color-text: #1e293b; 6 --color-text-muted: #64748b; 7 --color-border: #e2e8f0; 8 --color-primary: #3b82f6; 9 --shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 10} 11 12/* Dark theme */ 13[data-theme="dark"] { 14 --color-bg: #0f172a; 15 --color-surface: #1e293b; 16 --color-text: #f1f5f9; 17 --color-text-muted: #94a3b8; 18 --color-border: #334155; 19 --color-primary: #60a5fa; 20 --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 21} 22 23/* System preference */ 24@media (prefers-color-scheme: dark) { 25 :root:not([data-theme="light"]) { 26 --color-bg: #0f172a; 27 --color-surface: #1e293b; 28 --color-text: #f1f5f9; 29 --color-text-muted: #94a3b8; 30 --color-border: #334155; 31 --color-primary: #60a5fa; 32 --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 33 } 34} 35 36/* Components use semantic variables */ 37body { 38 background-color: var(--color-bg); 39 color: var(--color-text); 40} 41 42.card { 43 background-color: var(--color-surface); 44 border: 1px solid var(--color-border); 45 box-shadow: var(--shadow); 46}
1// Theme toggle in JavaScript 2function setTheme(theme: 'light' | 'dark' | 'system') { 3 if (theme === 'system') { 4 document.documentElement.removeAttribute('data-theme'); 5 localStorage.removeItem('theme'); 6 } else { 7 document.documentElement.setAttribute('data-theme', theme); 8 localStorage.setItem('theme', theme); 9 } 10} 11 12// Initialize on load 13function initTheme() { 14 const saved = localStorage.getItem('theme'); 15 if (saved) { 16 document.documentElement.setAttribute('data-theme', saved); 17 } 18} 19 20// React hook 21function useTheme() { 22 const [theme, setThemeState] = useState<'light' | 'dark' | 'system'>(() => { 23 if (typeof window === 'undefined') return 'system'; 24 return (localStorage.getItem('theme') as any) || 'system'; 25 }); 26 27 useEffect(() => { 28 setTheme(theme); 29 }, [theme]); 30 31 return { theme, setTheme: setThemeState }; 32}

Color Scales#

1:root { 2 /* Generate color scales */ 3 --gray-50: #f8fafc; 4 --gray-100: #f1f5f9; 5 --gray-200: #e2e8f0; 6 --gray-300: #cbd5e1; 7 --gray-400: #94a3b8; 8 --gray-500: #64748b; 9 --gray-600: #475569; 10 --gray-700: #334155; 11 --gray-800: #1e293b; 12 --gray-900: #0f172a; 13 14 --blue-50: #eff6ff; 15 --blue-100: #dbeafe; 16 --blue-200: #bfdbfe; 17 --blue-300: #93c5fd; 18 --blue-400: #60a5fa; 19 --blue-500: #3b82f6; 20 --blue-600: #2563eb; 21 --blue-700: #1d4ed8; 22 --blue-800: #1e40af; 23 --blue-900: #1e3a8a; 24 25 /* Semantic mappings */ 26 --color-text: var(--gray-900); 27 --color-text-secondary: var(--gray-600); 28 --color-primary: var(--blue-600); 29 --color-primary-hover: var(--blue-700); 30} 31 32[data-theme="dark"] { 33 --color-text: var(--gray-100); 34 --color-text-secondary: var(--gray-400); 35 --color-primary: var(--blue-400); 36 --color-primary-hover: var(--blue-300); 37}

Component Theming#

1/* Component-level variables */ 2.button { 3 --btn-bg: var(--color-primary); 4 --btn-text: white; 5 --btn-padding-x: var(--spacing-md); 6 --btn-padding-y: var(--spacing-sm); 7 --btn-radius: var(--radius-md); 8 9 background-color: var(--btn-bg); 10 color: var(--btn-text); 11 padding: var(--btn-padding-y) var(--btn-padding-x); 12 border-radius: var(--btn-radius); 13} 14 15.button:hover { 16 --btn-bg: var(--color-primary-hover); 17} 18 19/* Variants override component variables */ 20.button--secondary { 21 --btn-bg: var(--color-surface); 22 --btn-text: var(--color-text); 23} 24 25.button--outline { 26 --btn-bg: transparent; 27 --btn-text: var(--color-primary); 28 border: 1px solid var(--color-primary); 29} 30 31.button--sm { 32 --btn-padding-x: var(--spacing-sm); 33 --btn-padding-y: var(--spacing-xs); 34} 35 36.button--lg { 37 --btn-padding-x: var(--spacing-lg); 38 --btn-padding-y: var(--spacing-md); 39}

Dynamic Values with JavaScript#

1// Set CSS variables dynamically 2function setAccentColor(color: string) { 3 document.documentElement.style.setProperty('--color-accent', color); 4} 5 6// Read CSS variables 7function getVariable(name: string): string { 8 return getComputedStyle(document.documentElement) 9 .getPropertyValue(name) 10 .trim(); 11} 12 13// Generate color variations 14function setColorWithVariations(name: string, hsl: string) { 15 const root = document.documentElement; 16 17 root.style.setProperty(`--${name}`, `hsl(${hsl})`); 18 root.style.setProperty(`--${name}-light`, `hsl(${hsl} / 0.1)`); 19 root.style.setProperty(`--${name}-dark`, `hsl(${hsl} / 0.8)`); 20} 21 22// User-customizable theme 23interface ThemeConfig { 24 primaryColor: string; 25 accentColor: string; 26 borderRadius: string; 27 fontFamily: string; 28} 29 30function applyTheme(config: ThemeConfig) { 31 const root = document.documentElement.style; 32 33 root.setProperty('--color-primary', config.primaryColor); 34 root.setProperty('--color-accent', config.accentColor); 35 root.setProperty('--radius-md', config.borderRadius); 36 root.setProperty('--font-sans', config.fontFamily); 37}

Animation Variables#

1:root { 2 --duration-fast: 150ms; 3 --duration-normal: 300ms; 4 --duration-slow: 500ms; 5 6 --ease-in: cubic-bezier(0.4, 0, 1, 1); 7 --ease-out: cubic-bezier(0, 0, 0.2, 1); 8 --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); 9} 10 11/* Reduce motion preference */ 12@media (prefers-reduced-motion: reduce) { 13 :root { 14 --duration-fast: 0ms; 15 --duration-normal: 0ms; 16 --duration-slow: 0ms; 17 } 18} 19 20.fade-in { 21 animation: fadeIn var(--duration-normal) var(--ease-out); 22} 23 24.button { 25 transition: background-color var(--duration-fast) var(--ease-in-out); 26}

Responsive Variables#

1:root { 2 --container-width: 100%; 3 --sidebar-width: 0; 4 --header-height: 56px; 5 --font-size-base: 14px; 6} 7 8@media (min-width: 768px) { 9 :root { 10 --container-width: 750px; 11 --sidebar-width: 240px; 12 --header-height: 64px; 13 --font-size-base: 16px; 14 } 15} 16 17@media (min-width: 1024px) { 18 :root { 19 --container-width: 1000px; 20 --sidebar-width: 280px; 21 } 22} 23 24@media (min-width: 1280px) { 25 :root { 26 --container-width: 1200px; 27 } 28} 29 30.container { 31 max-width: var(--container-width); 32 margin: 0 auto; 33} 34 35.sidebar { 36 width: var(--sidebar-width); 37} 38 39body { 40 font-size: var(--font-size-base); 41}

Scoped Themes#

1/* Theme per section */ 2.marketing-section { 3 --color-primary: #8b5cf6; 4 --color-bg: #faf5ff; 5} 6 7.dashboard-section { 8 --color-primary: #0ea5e9; 9 --color-bg: #f0f9ff; 10} 11 12/* All children inherit scoped variables */ 13.marketing-section .button { 14 background: var(--color-primary); /* Purple */ 15} 16 17.dashboard-section .button { 18 background: var(--color-primary); /* Blue */ 19}

Best Practices#

Organization: ✓ Use semantic variable names ✓ Create color scales ✓ Group related variables ✓ Document variable purpose Theming: ✓ Support system preference ✓ Persist user choice ✓ Avoid flash of wrong theme ✓ Test both themes thoroughly Performance: ✓ Define variables at :root ✓ Avoid excessive recalculation ✓ Use fallback values ✓ Minimize JavaScript updates

Conclusion#

CSS custom properties create flexible, maintainable theming systems. Define semantic variables, support dark mode with system preference detection, and use component-level variables for reusable patterns. The result is a theming system that's easy to extend and maintain.

Share this article

Help spread the word about Bootspring