Skip to main content
design systems

The Difference Between a Theme and a Design System

A theme changes how things look. A design system changes how things are built. Conflating them produces a token file nobody trusts and components nobody reuses.


7 min read

A theme is a set of values — colors, spacing, type scales — that can be swapped. A design system is a set of decisions about patterns, behaviors, and APIs that encode how a product is built. You can have a theme without a design system. You can have a design system without a theme. Most teams that say they have a design system actually have a theme, and wonder why their components keep diverging.


What a Theme Actually Is

A theme is runtime-swappable configuration. CSS custom properties are the canonical mechanism:

:root {
  --color-brand: #5b21b6;
  --color-surface: #ffffff;
  --color-text-primary: #111827;
  --space-4: 1rem;
  --space-8: 2rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --radius-md: 0.375rem;
}

[data-theme="dark"] {
  --color-brand: #7c3aed;
  --color-surface: #0f172a;
  --color-text-primary: #f8fafc;
}

This is useful. When you apply data-theme="dark" to <html>, every component that references --color-surface switches automatically. No JavaScript, no re-render, no class toggling on 200 elements. This is a solved problem and CSS custom properties solve it well.

What a theme does not give you: decisions about composition, behavior, accessibility patterns, or API contracts. Those belong to the design system.

What a Design System Actually Is

A design system is accumulated decisions. It answers questions like:

  • When does an action trigger a modal vs a drawer vs a new page?
  • What are the rules for when a form shows inline errors vs toast notifications?
  • How does a skeleton loader behave differently from a spinner, and when do you use each?
  • What is the keyboard navigation contract for a dropdown menu?

These decisions are not in a token file. They live in component implementations, in documented patterns, in review culture, in the questions your team stops having to debate because they were settled once.

A design system without a theme is still useful — it just has hardcoded values. A theme without a design system is a CSS variable file that gets ignored the moment a component needs to do something slightly complicated.

Why Teams Conflate Them

The conflation happens because both artifacts come from the same source: a design tool. A Figma file can contain both a color style library (theme) and a component library (design system). When engineers receive "the design file," they tend to extract the token values and call the job done.

The result is a tokens.css file and a Figma component kit that nobody has actually implemented. Teams then build components independently, each making slightly different decisions about hover states, focus rings, disabled styling, and spacing. The theme is applied. The design system never ships.

The token file becomes untrustworthy because nobody owns the contract between the token and its intended usage. Is --color-brand the right token for a destructive action button? Probably not. But if there is no design system, there is no answer — just a guess.

What CSS Custom Properties Do (and Do Not Do)

CSS custom properties propagate values through the cascade. That is all they do. They do not:

  • Enforce that --color-brand is only used for brand interactions, not error states
  • Prevent a component from applying --space-8 where --space-4 was intended
  • Generate consistent component spacing from a spacing scale
  • Define what "medium" means for a component size

The gap between "we have tokens" and "we have a design system" is the gap between having values and having rules for using those values. Tokens without rules are suggestions.

Tailwind's configuration gets closer by making the token-to-utility mapping explicit — bg-brand-500 is generated from your config, and there is no raw hex value floating around. But even that does not solve the question of when to use bg-brand-500 versus bg-brand-600.

When Theming Is the Right Solution

Theming solves specific problems well:

Dark mode. If your product has a color scheme preference, CSS custom properties with prefers-color-scheme and a [data-theme] attribute is the correct solution. It requires no JavaScript in the critical path.

White-labeling. If you are building SaaS for enterprise customers who need their brand colors, a theme layer lets you swap the token set per tenant without touching component code.

Context-specific overrides. A marketing page section that needs a dark background can use a scoped theme without rewriting components.

These are all real use cases. The mistake is treating theming as a substitute for system-level decisions.

When "Just Use CSS Variables" Is a Workaround

The most common workaround I see: a team has inconsistent components, design wants consistency, the proposed solution is "let's add CSS variables and everyone will use the right colors." This solves exactly nothing about structural inconsistency.

/* Before: hardcoded, inconsistent */
.button-primary { background: #5b21b6; }
.cta-button { background: #6d28d9; } /* slightly different */
.submit-btn { background: var(--purple-600); } /* different scale */

/* After: tokenized, still inconsistent */
.button-primary { background: var(--color-brand); }
.cta-button { background: var(--color-brand-hover); } /* wrong semantic */
.submit-btn { background: var(--color-brand); } /* now consistent, but... */

The color is now consistent. The component API is still three different things. The behavioral decisions — what happens on hover, on focus, on disabled — are still made independently in each component. Theming fixed the symptom. The design system problem remains.

The Code That Shows the Difference

Here is what a theme looks like in code:

// theme.ts — values only
export const theme = {
  colors: {
    brand: "var(--color-brand)",
    surface: "var(--color-surface)",
    textPrimary: "var(--color-text-primary)",
  },
  space: {
    4: "var(--space-4)",
    8: "var(--space-8)",
  },
} as const;

Here is what a design system component adds on top of that:

// Button.tsx — decisions encoded in code
const buttonVariants = cva(
  // Base styles that apply to every button
  "inline-flex items-center justify-center rounded-[var(--radius-md)] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        solid: "bg-[var(--color-brand)] text-white hover:bg-[var(--color-brand-hover)]",
        outline: "border border-[var(--color-brand)] text-[var(--color-brand)] hover:bg-[var(--color-brand-subtle)]",
        ghost: "text-[var(--color-brand)] hover:bg-[var(--color-brand-subtle)]",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-base",
        lg: "h-12 px-6 text-lg",
      },
    },
    defaultVariants: {
      variant: "solid",
      size: "md",
    },
  }
);

The theme answers "what color is brand?" The design system answers "how does a button use brand color, and in what size, with what focus behavior, and what does disabled mean?" These are different questions. Answering only the first one and calling it done is the mistake.

The Practical Test

If your "design system" cannot answer these questions without "it depends on the component" — you have a theme, not a system:

  1. What is the focus ring style for interactive elements?
  2. What spacing separates a label from its input?
  3. How does an error state display in a form field?
  4. What is the minimum touch target size for interactive elements?

A theme does not have opinions about any of this. A design system has committed answers.

The work of building a real design system is slower and harder than publishing a token file. It requires shipping components with encoded decisions, writing documentation for patterns not just values, and doing review that enforces the contract. It is also the only thing that actually produces consistency at scale.

Start with the theme. It is necessary and it is a foundation. But do not mistake the foundation for the building.