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