Design System Patterns with Svelte

Tokens, Theming, and SSR — practical patterns for building a robust, performant design system with Svelte and SvelteKit

Design Systems Svelte SSR

Overview

This article shows a pragmatic approach to building a design system in Svelte that scales across teams and supports server-side rendering. You’ll learn how to model tokens, implement theming that works during SSR and hydration, and structure components for accessibility and performance.

Why Tokens and Theming Matter

Tokens are the single source of truth for visual decisions. Theming lets you adapt those tokens for brands, user preferences, or runtime contexts. When combined with SSR, tokens must be available at render time so the initial HTML/CSS matches the hydrated client state and avoids flashes or layout shifts.

Core Patterns

1. Token Source of Truth

Keep tokens in a single place and export them in formats consumable by CSS, JS, and design tools. A common pattern is to author tokens in JSON or TypeScript and generate CSS variables and a JS token map from that source.

// tokens.ts
export const tokens = {
  color: {
    background: "#0b1220",
    surface: "#0f1724",
    text: "#e6eef6",
    primary: "#7c5cff",
    muted: "#9aa4b2"
  },
  radius: {
    sm: "6px",
    md: "12px",
    lg: "20px"
  },
  spacing: {
    xs: "4px",
    sm: "8px",
    md: "16px",
    lg: "24px"
  }
}

From this file you can generate a CSS variables file for runtime use and also export a JS object for component logic and tests.

2. CSS Variables for Runtime Theming

Expose tokens as CSS variables so components can style themselves without JS. This is essential for SSR because the server can render HTML with the correct variables inline or in a critical stylesheet.

/* tokens.css generated from tokens.ts */
:root {
  --color-background: #0b1220;
  --color-surface: #0f1724;
  --color-text: #e6eef6;
  --color-primary: #7c5cff;
  --radius-md: 12px;
  --spacing-md: 16px;
}

For theming, create theme variants that override a subset of variables.

/* theme-dark.css */
:root[data-theme="dark"] {
  --color-background: #0b1220;
  --color-text: #e6eef6;
}

/* theme-light.css */
:root[data-theme="light"] {
  --color-background: #ffffff;
  --color-text: #0b1b2b;
}

3. Svelte Store for Theme State

Use a Svelte store to manage theme state on the client. Keep the store SSR-safe by initializing it from server-provided data (cookie, inline script, or load function).

// src/lib/theme.ts
import { writable } from 'svelte/store';

export type Theme = 'light' | 'dark';

export const theme = writable<Theme>('dark');

Subscribe in a root layout component to apply the data-theme attribute.

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { theme } from '$lib/theme';
  export let data;
  // data.theme comes from server load
  theme.set(data.theme ?? 'dark');
  onMount(() => {
    const unsubscribe = theme.subscribe(t => {
      document.documentElement.setAttribute('data-theme', t);
    });
  });
</script>

<slot />

4. SSR Initialization Strategies

To avoid a flash of incorrect theme during hydration, ensure the server renders the correct theme into the HTML. Common approaches:

  • Server load — read a theme cookie in your SvelteKit load function and pass it to the layout.
  • Inline script — emit a small inline script that sets data-theme before CSS loads (useful when cookies are not available).
  • Critical CSS — inline the critical CSS for the initial theme to avoid FOUC.
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ cookies }) => {
  const theme = cookies.get('theme') ?? 'dark';
  return { theme };
};

5. Component Patterns

Design system components should prefer CSS variables for visual properties and accept token keys for API ergonomics.

<!-- src/lib/Button.svelte -->
<script lang="ts">
  export let variant: 'primary' | 'ghost' = 'primary';
</script>

<button class="btn {variant}">
  <slot />
</button>

<style>
.btn{
  background:var(--color-primary);
  color:var(--color-text);
  padding:var(--spacing-md);
  border-radius:var(--radius-md);
  border:none;
}
.btn.ghost{
  background:transparent;
  color:var(--color-primary);
  border:1px solid rgba(255,255,255,0.06);
}
</style>

Because the component uses CSS variables, it automatically adapts to the active theme without runtime logic inside the component.

Advanced Topics

Token Transformations and Build Tooling

Use a build step to generate multiple outputs from a single token source: CSS variables, JSON for design tools, and a TypeScript token map. Tools like Style Dictionary or custom scripts work well. Keep the token source authoritative and regenerate artifacts on change.

Scoped Theming and Component Isolation

For component libraries consumed by multiple apps, support both global theming and scoped themes. Scoped themes can be applied to a container element using a data-theme attribute or a CSS class.

<div data-theme="brand-a">
  <MyComponent />
</div>

Accessibility and Contrast

Include accessibility tokens (focus ring, motion, reduced motion) and validate color contrast for each theme. Expose tokens for focus outlines and motion preferences so components can respect user settings.

Testing and Visual Regression

Test components across themes and token variants. Use snapshot testing and visual regression tools (Playwright, Chromatic, Percy) to catch regressions when tokens change.

Performance and SSR Considerations

Key goals: avoid FOUC, minimize runtime work, and keep initial payload small.

  • Inline critical variables for the initial theme in the server-rendered HTML when possible.
  • Defer noncritical CSS and lazy-load theme-specific assets for alternate themes.
  • Prefer CSS variables over JS-driven style updates to reduce hydration work.
  • Cache theme decisions at the CDN or edge when themes are tied to user segments.

Example Repo Structure

design-system/
├─ tokens/
│  ├─ tokens.ts        // canonical token source
│  ├─ build.js         // generates tokens.css and tokens.json
├─ packages/
│  ├─ components/      // Svelte component library
│  │  ├─ Button.svelte
│  │  └─ index.ts
│  └─ utils/
│     └─ theme.ts
├─ app/                // SvelteKit app consuming the design system
│  ├─ src/
│  │  ├─ lib/
│  │  └─ routes/
│  └─ static/
└─ scripts/
   └─ generate-tokens.js

This layout separates token generation, component code, and the application that consumes the design system.

Deployment Checklist

  • Ensure server load reads theme cookie and passes it to layout.
  • Inline minimal theme variables to prevent FOUC.
  • Generate and ship a small critical CSS bundle for the initial render.
  • Run visual regression tests across themes before release.
  • Document token changes and provide migration notes for consumers.