We will create a dynamic theme system with CSS variables to create a dark, normal or branded theme.
We define the structure of variables at :root.
No use hardcoded colors in components. Only use semantic variables.
:root { /* Color scheme */ --color-bg: #ffffff; --color-surface: #f5f5f5; --color-primary: #1976d2; --color-text: #222222; --color-text-muted: #666666; /* Borders */ --radius-md: 8px; /* Shadows */ --shadow-md: 0 4px 10px rgba(0, 0, 0, 0.1); }
Professional Key:
No call a variable --blue. Call it --color-primary.
This way you can change the color without changing its meaning.
Now we redefine only the necessary variables under a class.
.theme-dark { --color-bg: #121212; --color-surface: #1e1e1e; --color-primary: #90caf9; --color-text: #ffffff; --color-text-muted: #bbbbbb; --shadow-md: 0 4px 10px rgba(0, 0, 0, 0.4); }
Notice: we didn’t duplicate styles, only changed variables.
Imagine you want a “startup green” mode or a different brand.
.theme-brand-green { --color-primary: #2e7d32; } .theme-brand-purple { --color-primary: #6a1b9a; }
You can combine classes:
<body class="theme-dark theme-brand-purple">
This allows:Dark mode + purple brand
Light mode + green brand
No duplicated CSS combinations
Evaluation of styles:
body { background-color: var(--color-bg); color: var(--color-text); transition: background-color 0.3s ease, color 1.0s linear; } .card { background-color: var(--color-surface); border-radius: var(--radius-md); box-shadow: var(--shadow-md); padding: 16px; } .button { background-color: var(--color-primary); color: white; border: none; border-radius: var(--radius-md); padding: 10px 16px; cursor: pointer; }
OBSERVATION: No component knows whether it is in dark or light mode.
Only use variables.
That’s clean architecture.
Now it gets interesting.
function setTheme(themeName) { document.body.className = themeName; }
Example buttons:
<button onclick="setTheme('')">Light</button> <button onclick="setTheme('theme-dark')">Dark</button> <button onclick="setTheme('theme-dark theme-brand-purple')"> Dark Purple </button>
- Persist the user’s preference
This is where it sounds like a real product.
function setTheme(themeName) { document.body.className = themeName; localStorage.setItem("theme", themeName); } function loadTheme() { const savedTheme = localStorage.getItem("theme"); if (savedTheme) { document.body.className = savedTheme; } } loadTheme();
The user returns and maintains the theme.
If you want to do it on a “company level”:
function detectSystemTheme() { const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; return prefersDark ? "theme-dark" : ""; } function loadTheme() { const savedTheme = localStorage.getItem("theme"); if (savedTheme) { document.body.className = savedTheme; } else { document.body.className = detectSystemTheme(); } } loadTheme();
This makes the first render respect the user’s system.
Empty structure:
styles
tokens.css
themes.css
components.css
tokens.css → base variables
themes.css → theme-dark and theme-brand classes
components.css → styles using var()
Clear separation. Scalable.
Classic problem:
The page loads in light and then switches to dark.
Solution:
Inject the script before rendering:
<script> const savedTheme = localStorage.getItem("theme"); if (savedTheme) { document.documentElement.className = savedTheme; } </script>
