Creating a Dynamic Theme System (Light/Dark/Brand) Professionally

Tiempo de lectura: 2 minutos

We will create a dynamic theme system with CSS variables to create a dark, normal or branded theme.

Frozen Lake - pexels

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> 
  1. 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>

Leave a Comment