Skip to Content
Livery is in early development. Star us on GitHub!
Why Livery?

Why Livery?

Most theming approaches require you to specify styles for each theme separately. You end up with something like this:

<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white"> <h1 className="text-2xl font-bold text-slate-800 dark:text-slate-100"> Welcome </h1> <p className="text-slate-600 dark:text-slate-400"> This is getting repetitive. </p> <button className="bg-blue-500 dark:bg-blue-400 hover:bg-blue-600 dark:hover:bg-blue-500 text-white"> Click me </button> </div>

Every element. Every color. Every state. Duplicated. This isn’t a problem with any particular tool — it’s just how theming typically works when you reference specific color values instead of semantic tokens.

The Semantic Alternative

With Livery, you define your colors once as semantic tokens. Then you use those tokens everywhere:

<div className="bg-background text-foreground"> <h1 className="text-2xl font-bold"> Welcome </h1> <p className="text-muted"> Much cleaner. </p> <button className="bg-primary hover:bg-primary-hover text-white"> Click me </button> </div>

Same markup for light mode, dark mode, or any theme you create. The meaning is clear: bg-background is the background, text-muted is muted text. When the theme changes, the CSS variables update — your markup stays the same.

This works because Livery generates CSS variables (--colors-background, --colors-primary, etc.) that you can use directly or map to your styling tool of choice.

How It Works

Define your design tokens with full TypeScript inference:

import { createSchema, t, toCssStringAll, type InferTheme } from '@livery/core'; const schema = createSchema({ definition: { colors: { primary: t.color(), primaryHover: t.color(), background: t.color(), foreground: t.color(), muted: t.color(), }, }, }); type Theme = InferTheme<typeof schema.definition>; const light: Theme = { colors: { primary: '#14B8A6', primaryHover: '#0D9488', background: '#FFFFFF', foreground: '#0F172A', muted: '#64748B', }, }; const dark: Theme = { colors: { primary: '#2DD4BF', primaryHover: '#14B8A6', background: '#0F172A', foreground: '#F8FAFC', muted: '#94A3B8', }, }; // Generate CSS for all themes at once const css = toCssStringAll({ schema, themes: { light, dark }, defaultTheme: 'light', });

Use the CSS variables directly, or map them to your framework. For example, with Tailwind:

@theme { --color-primary: var(--colors-primary); --color-primary-hover: var(--colors-primaryHover); --color-background: var(--colors-background); --color-foreground: var(--colors-foreground); --color-muted: var(--colors-muted); }

Now bg-primary, text-foreground, and text-muted just work — in any theme.

The Real Benefits

1. Write Once, Theme Anywhere

Your components don’t know or care which theme is active. They use semantic names like bg-background and the CSS variables handle the rest.

2. Type-Safe Design Tokens

Typo in a color name? TypeScript catches it. Missing a required token in your theme? TypeScript catches it. Your schema is the source of truth.

const broken: Theme = { colors: { primary: '#14B8A6', // TypeScript error: missing 'background', 'foreground', etc. }, };

3. Scales Beyond Light/Dark

Start with light and dark. Later, add high-contrast, sepia, or customer-specific themes. Your components don’t change — just add new theme objects.

4. Works With Your Stack

Livery outputs CSS variables. Use them with Tailwind, vanilla CSS, CSS-in-JS, or anything that understands var(--colors-primary).

Works with any React setup — SSR or client-side, bundled themes or fetched from APIs.

Where Livery Really Shines

Light/dark mode is just the beginning. The same schema and type safety scales to more complex scenarios:

Multi-tenant SaaS — Each customer gets their own brand colors, loaded from your database:

const resolver = createResolver({ schema, fetcher: async (themeId) => { const theme = await db.themes.findUnique({ where: { themeId } }); return theme ?? defaultTheme; }, });

White-label products — Partners fully customize the look and feel. Same app, completely different branding.

User preferences — Let users pick their accent color or toggle high-contrast mode. Validate with the same schema.

A/B testing — Test different visual treatments. Each variant is just a theme object.

The mental model stays the same: define a schema, create themes that match it, use semantic tokens in your components. Whether you have 2 themes or 200, your components don’t change.

See Multi-Tenant Example →

When to Use Livery

ScenarioRecommendation
Simple light/dark, don’t mind the duplicationYou can manage it manually
Semantic design system with themesLivery
Multi-tenant / white-label appsLivery
Type-safe design tokensLivery
User-customizable themesLivery

If you’re happy with bg-white dark:bg-slate-900 on every element, you don’t need Livery. But if you want bg-background to mean the right thing in every context, Livery makes that easy.

Next Steps

Last updated on