Theming
This page is for frontend developers or designer-developers customizing a storefront's visual appearance for a specific customer. The most common scenario is whitelabeling: the same storefront codebase serves multiple customers (or multiple brands within one customer), each with their own colors, typography, and logo — and you want to handle that without forking the codebase or rebuilding for each variant.
The mechanism is CSS custom properties (CSS variables) applied on :root. Components reference variables (var(--color-primary)) rather than hard-coded values; switching brand means changing the variable values, not the component code. This is well-supported in all modern browsers, well-understood by designers (who can write the design tokens directly), and works at runtime — no build step per customer.
The rest of this page shows the pattern. The specific token list will depend on which storefront template you start from.
How Theming Works
A storefront defines its visual design in CSS custom properties on :root:
:root {
/* Colors */
--color-primary: #4f46e5;
--color-primary-hover: #4338ca;
--color-secondary: #10b981;
--color-background: #ffffff;
--color-surface: #f9fafb;
--color-text: #1f2937;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-error: #ef4444;
--color-success: #10b981;
/* Typography */
--font-family-base: 'Inter', system-ui, sans-serif;
--font-family-heading: 'Inter', system-ui, sans-serif;
--font-size-base: 1rem;
--line-height-base: 1.5;
/* Spacing */
--spacing-unit: 0.25rem;
--container-max-width: 1280px;
/* Borders & radii */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
}
Components reference these variables rather than hard-coded values:
.button-primary {
background: var(--color-primary);
color: white;
border-radius: var(--radius-md);
padding: calc(var(--spacing-unit) * 3) calc(var(--spacing-unit) * 6);
font-family: var(--font-family-base);
}
.button-primary:hover {
background: var(--color-primary-hover);
}
Theme Switching
Override variables on a [data-theme] attribute to enable runtime theme switching:
[data-theme="dark"] {
--color-background: #111827;
--color-surface: #1f2937;
--color-text: #f3f4f6;
--color-text-muted: #9ca3af;
--color-border: #374151;
}
<script setup lang="ts">
const theme = useState<'light' | 'dark'>('theme', () => 'light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', theme.value)
localStorage.setItem('theme', theme.value)
}
onMounted(() => {
const saved = localStorage.getItem('theme') as 'light' | 'dark' | null
if (saved) {
theme.value = saved
document.documentElement.setAttribute('data-theme', saved)
}
})
</script>
<template>
<button @click="toggleTheme" aria-label="Toggle theme">
{{ theme === 'dark' ? '☀️' : '🌙' }}
</button>
</template>
Per-Tenant Theming
In a multi-tenant storefront, each tenant can have its own theme tokens. Fetch the active tenant's theme during SSR and inject it into the page:
<script setup lang="ts">
const route = useRoute()
const { data: theme } = await useFetch(`/api/tenant/${route.params.tenant}/theme`)
</script>
<template>
<div :style="theme.cssVars">
<slot />
</div>
</template>
The server returns a tenant-specific style object:
// server/api/tenant/[id]/theme.get.ts
export default defineEventHandler(async (event) => {
const tenantId = getRouterParam(event, 'id')
const tenant = await cockpit.tenants.findById(tenantId)
return {
cssVars: {
'--color-primary': tenant.branding.primary_color,
'--color-secondary': tenant.branding.secondary_color,
'--font-family-base': tenant.branding.font_family
}
}
})
Logos and Assets
For tenant-specific logos, favicons, and Open Graph images, serve them via a CDN or the storefront's media API:
<template>
<NuxtImg
:src="tenant.branding.logo_url"
:alt="`${tenant.name} logo`"
width="160"
height="40"
/>
</template>
Best Practices
A theming system stays maintainable when the tokens are few, semantic, and centralized — and tends to rot when they're many, literal, and scattered. A few habits keep it on the right side of that line.
Name tokens for their role, not their value. A --color-primary survives a rebrand from blue to green; a --color-blue-500 becomes a lie the moment the brand colour changes. Keep the set small while you're at it — define twenty or thirty core tokens and derive the rest from them with CSS functions like color-mix and calc, rather than hand-maintaining a hundred.
Keep theming in one place, and verify it everywhere. Centralize the tokens in something like assets/css/theme.css rather than scattering overrides through component <style scoped> blocks, so there's a single source of truth. Test in both light and dark mode before shipping — a contrast regression is easy to miss in one mode and obvious in the other. And because tenant tokens change rarely, fetch them once per session and cache aggressively server-side rather than on every render.
Next Steps
- Storefront Architecture — Overall stack and structure
- Authentication — Apply themes per logged-in customer
- Blökkli Blocks — Visual page composition