Skip to Content
Livery is in early development. Star us on GitHub!
Stylingshadcn/ui

Shadcn/ui Integration

Shadcn/ui  is a collection of reusable components built with Radix UI and Tailwind CSS. It uses CSS variables for theming, making it a perfect match for Livery.

How It Works

Shadcn components use CSS variables like --primary, --background, --foreground, etc. Livery outputs CSS variables that map directly to these, giving you type-safe theming for all Shadcn components.

Setup

1. Define a Shadcn-Compatible Schema

Create a schema that matches Shadcn’s expected CSS variables:

lib/theme.ts
import { createSchema, t, toCssStringAll, type InferTheme } from '@livery/core'; export const schema = createSchema({ definition: { // Base colors background: t.color(), foreground: t.color(), // Card card: t.color(), cardForeground: t.color(), // Popover popover: t.color(), popoverForeground: t.color(), // Primary primary: t.color(), primaryForeground: t.color(), // Secondary secondary: t.color(), secondaryForeground: t.color(), // Muted muted: t.color(), mutedForeground: t.color(), // Accent accent: t.color(), accentForeground: t.color(), // Destructive destructive: t.color(), destructiveForeground: t.color(), // Border, input, ring border: t.color(), input: t.color(), ring: t.color(), // Radius radius: t.dimension(), }, }); type Theme = InferTheme<typeof schema.definition>; const light: Theme = { background: 'hsl(0 0% 100%)', foreground: 'hsl(222.2 84% 4.9%)', card: 'hsl(0 0% 100%)', cardForeground: 'hsl(222.2 84% 4.9%)', popover: 'hsl(0 0% 100%)', popoverForeground: 'hsl(222.2 84% 4.9%)', primary: 'hsl(222.2 47.4% 11.2%)', primaryForeground: 'hsl(210 40% 98%)', secondary: 'hsl(210 40% 96.1%)', secondaryForeground: 'hsl(222.2 47.4% 11.2%)', muted: 'hsl(210 40% 96.1%)', mutedForeground: 'hsl(215.4 16.3% 46.9%)', accent: 'hsl(210 40% 96.1%)', accentForeground: 'hsl(222.2 47.4% 11.2%)', destructive: 'hsl(0 84.2% 60.2%)', destructiveForeground: 'hsl(210 40% 98%)', border: 'hsl(214.3 31.8% 91.4%)', input: 'hsl(214.3 31.8% 91.4%)', ring: 'hsl(222.2 84% 4.9%)', radius: '0.5rem', }; const dark: Theme = { background: 'hsl(222.2 84% 4.9%)', foreground: 'hsl(210 40% 98%)', card: 'hsl(222.2 84% 4.9%)', cardForeground: 'hsl(210 40% 98%)', popover: 'hsl(222.2 84% 4.9%)', popoverForeground: 'hsl(210 40% 98%)', primary: 'hsl(210 40% 98%)', primaryForeground: 'hsl(222.2 47.4% 11.2%)', secondary: 'hsl(217.2 32.6% 17.5%)', secondaryForeground: 'hsl(210 40% 98%)', muted: 'hsl(217.2 32.6% 17.5%)', mutedForeground: 'hsl(215 20.2% 65.1%)', accent: 'hsl(217.2 32.6% 17.5%)', accentForeground: 'hsl(210 40% 98%)', destructive: 'hsl(0 62.8% 30.6%)', destructiveForeground: 'hsl(210 40% 98%)', border: 'hsl(217.2 32.6% 17.5%)', input: 'hsl(217.2 32.6% 17.5%)', ring: 'hsl(212.7 26.8% 83.9%)', radius: '0.5rem', }; export const themesCss = toCssStringAll({ schema, themes: { light, dark }, defaultTheme: 'light', options: { prefix: '' }, // No prefix for Shadcn compatibility });

2. Configure Tailwind v4

Map Livery’s CSS variables to Tailwind utilities:

app/globals.css
@import 'tailwindcss'; @theme { /* Colors from Livery CSS variables */ --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--cardForeground); --color-popover: var(--popover); --color-popover-foreground: var(--popoverForeground); --color-primary: var(--primary); --color-primary-foreground: var(--primaryForeground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondaryForeground); --color-muted: var(--muted); --color-muted-foreground: var(--mutedForeground); --color-accent: var(--accent); --color-accent-foreground: var(--accentForeground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructiveForeground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); /* Border radius from Livery */ --radius-lg: var(--radius); --radius-md: calc(var(--radius) - 2px); --radius-sm: calc(var(--radius) - 4px); }

3. Add to Your Layout

Inject the theme CSS and read the user’s preference:

app/layout.tsx
import { cookies } from 'next/headers'; import { themesCss } from '@/lib/theme'; import './globals.css'; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const cookieStore = await cookies(); const theme = cookieStore.get('theme')?.value ?? 'light'; return ( <html lang="en" data-theme={theme}> <head> <style dangerouslySetInnerHTML={{ __html: themesCss }} /> </head> <body className="bg-background text-foreground"> {children} </body> </html> ); }

4. Add Theme Toggle

components/theme-toggle.tsx
'use client'; import { useState } from 'react'; import { Moon, Sun } from 'lucide-react'; import { Button } from '@/components/ui/button'; type ThemeId = 'light' | 'dark'; export function ThemeToggle({ initialTheme }: { initialTheme: ThemeId }) { const [theme, setTheme] = useState<ThemeId>(initialTheme); const isDark = theme === 'dark'; function toggle() { const next: ThemeId = isDark ? 'light' : 'dark'; document.documentElement.dataset.theme = next; document.cookie = `theme=${next};path=/;max-age=31536000`; setTheme(next); } return ( <Button variant="ghost" size="icon" onClick={toggle}> {isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />} <span className="sr-only">Toggle theme</span> </Button> ); }

That’s it. All Shadcn components now use your Livery theme values.

Multi-Tenant Theming

Need different themes per customer? Use a resolver to fetch themes dynamically:

lib/livery.ts
import { createResolver } from '@livery/core'; import { createDynamicThemeProvider } from '@livery/react'; import { schema } from './theme'; import { db } from './db'; export const resolver = createResolver({ schema, fetcher: async ({ themeId }) => { const record = await db.theme.findUnique({ where: { themeId }, }); if (!record) { return {}; // Falls back to schema defaults } return record.tokens; }, cache: { ttl: 60 * 1000, staleWhileRevalidate: true, }, }); export const { DynamicThemeProvider, useTheme, useThemeValue, } = createDynamicThemeProvider({ schema });
app/layout.tsx
import { DynamicThemeProvider, resolver } from '@/lib/livery'; import { getThemeIdFromHost } from '@/lib/theme'; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const themeId = await getThemeIdFromHost(); return ( <html lang="en"> <body> <DynamicThemeProvider themeId={themeId} resolver={resolver} cssOptions={{ prefix: '' }} > {children} </DynamicThemeProvider> </body> </html> ); }

See the Multi-Tenant Example for a complete implementation.

Benefits

  • Type safety — Full TypeScript inference for all theme values
  • Zero runtime cost — For static themes, all CSS is generated upfront
  • All Shadcn components — Every component automatically uses your theme
  • SSR support — No flash of unstyled content
Last updated on