Theming

Customize your storefront's appearance with CSS custom properties
The exact set of theme tokens and built-in themes is implementation-specific and may change. Treat the examples below as the general pattern; refer to your storefront template for the authoritative token list.

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:

CSS
: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:

CSS
.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:

CSS
[data-theme="dark"] {
  --color-background: #111827;
  --color-surface: #1f2937;
  --color-text: #f3f4f6;
  --color-text-muted: #9ca3af;
  --color-border: #374151;
}
Vue
<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:

Vue
<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:

TypeScript
// 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:

Vue
<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

Was this page helpful?