tailwind-design-system · diff
git:20260326.db33dbc to git:20260522.be57c0b
2 added, 381 removed. Audit A to A.
---
name: tailwind-design-system
description: Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.
---
# Tailwind Design System (v4)
Build production-ready design systems with Tailwind CSS v4, including CSS-first configuration, design tokens, component variants, responsive patterns, and accessibility.
> **Note**: This skill targets Tailwind CSS v4 (2024+). For v3 projects, refer to the [upgrade guide](https://tailwindcss.com/docs/upgrade-guide).
## When to Use This Skill
- Creating a component library with Tailwind v4
- Implementing design tokens and theming with CSS-first configuration
- Building responsive and accessible components
- Standardizing UI patterns across a codebase
- Migrating from Tailwind v3 to v4
- Setting up dark mode with native CSS features
## Key v4 Changes
| v3 Pattern | v4 Pattern |
| ------------------------------------- | --------------------------------------------------------------------- |
| `tailwind.config.ts` | `@theme` in CSS |
| `@tailwind base/components/utilities` | `@import "tailwindcss"` |
| `darkMode: "class"` | `@custom-variant dark (&:where(.dark, .dark *))` |
| `theme.extend.colors` | `@theme { --color-*: value }` |
| `require("tailwindcss-animate")` | CSS `@keyframes` in `@theme` + `@starting-style` for entry animations |
## Quick Start
```css
/* app.css - Tailwind v4 CSS-first configuration */
@import "tailwindcss";
/* Define your theme with @theme */
@theme {
/* Semantic color tokens using OKLCH for better color perception */
--color-background: oklch(100% 0 0);
--color-foreground: oklch(14.5% 0.025 264);
--color-primary: oklch(14.5% 0.025 264);
--color-primary-foreground: oklch(98% 0.01 264);
--color-secondary: oklch(96% 0.01 264);
--color-secondary-foreground: oklch(14.5% 0.025 264);
--color-muted: oklch(96% 0.01 264);
--color-muted-foreground: oklch(46% 0.02 264);
--color-accent: oklch(96% 0.01 264);
--color-accent-foreground: oklch(14.5% 0.025 264);
--color-destructive: oklch(53% 0.22 27);
--color-destructive-foreground: oklch(98% 0.01 264);
--color-border: oklch(91% 0.01 264);
--color-ring: oklch(14.5% 0.025 264);
--color-card: oklch(100% 0 0);
--color-card-foreground: oklch(14.5% 0.025 264);
/* Ring offset for focus states */
--color-ring-offset: oklch(100% 0 0);
/* Radius tokens */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
/* Animation tokens - keyframes inside @theme are output when referenced by --animate-* variables */
--animate-fade-in: fade-in 0.2s ease-out;
--animate-fade-out: fade-out 0.2s ease-in;
--animate-slide-in: slide-in 0.3s ease-out;
--animate-slide-out: slide-out 0.3s ease-in;
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateY(-0.5rem);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slide-out {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-0.5rem);
opacity: 0;
}
}
}
/* Dark mode variant - use @custom-variant for class-based dark mode */
@custom-variant dark (&:where(.dark, .dark *));
/* Dark mode theme overrides */
.dark {
--color-background: oklch(14.5% 0.025 264);
--color-foreground: oklch(98% 0.01 264);
--color-primary: oklch(98% 0.01 264);
--color-primary-foreground: oklch(14.5% 0.025 264);
--color-secondary: oklch(22% 0.02 264);
--color-secondary-foreground: oklch(98% 0.01 264);
--color-muted: oklch(22% 0.02 264);
--color-muted-foreground: oklch(65% 0.02 264);
--color-accent: oklch(22% 0.02 264);
--color-accent-foreground: oklch(98% 0.01 264);
--color-destructive: oklch(42% 0.15 27);
--color-destructive-foreground: oklch(98% 0.01 264);
--color-border: oklch(22% 0.02 264);
--color-ring: oklch(83% 0.02 264);
--color-card: oklch(14.5% 0.025 264);
--color-card-foreground: oklch(98% 0.01 264);
--color-ring-offset: oklch(14.5% 0.025 264);
}
/* Base styles */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground antialiased;
}
}
```
## Core Concepts
### 1. Design Token Hierarchy
```
Brand Tokens (abstract)
└── Semantic Tokens (purpose)
└── Component Tokens (specific)
Example:
oklch(45% 0.2 260) → --color-primary → bg-primary
```
### 2. Component Architecture
```
Base styles → Variants → Sizes → States → Overrides
```
- ## Patterns
-
- ### Pattern 1: CVA (Class Variance Authority) Components
-
- ```typescript
- // components/ui/button.tsx
- import { Slot } from '@radix-ui/react-slot'
- import { cva, type VariantProps } from 'class-variance-authority'
- import { cn } from '@/lib/utils'
-
- const buttonVariants = cva(
- // Base styles - v4 uses native CSS variables
- 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
- outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground',
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- link: 'text-primary underline-offset-4 hover:underline',
- },
- size: {
- default: 'h-10 px-4 py-2',
- sm: 'h-9 rounded-md px-3',
- lg: 'h-11 rounded-md px-8',
- icon: 'size-10',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- }
- )
-
- export interface ButtonProps
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
- VariantProps<typeof buttonVariants> {
- asChild?: boolean
- }
-
- // React 19: No forwardRef needed
- export function Button({
- className,
- variant,
- size,
- asChild = false,
- ref,
- ...props
- }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) {
- const Comp = asChild ? Slot : 'button'
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- ref={ref}
- {...props}
- />
- )
- }
-
- // Usage
- <Button variant="destructive" size="lg">Delete</Button>
- <Button variant="outline">Cancel</Button>
- <Button asChild><Link href="/home">Home</Link></Button>
- ```
-
- ### Pattern 2: Compound Components (React 19)
-
- ```typescript
- // components/ui/card.tsx
- import { cn } from '@/lib/utils'
-
- // React 19: ref is a regular prop, no forwardRef
- export function Card({
- className,
- ref,
- ...props
- }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) {
- return (
- <div
- ref={ref}
- className={cn(
- 'rounded-lg border border-border bg-card text-card-foreground shadow-sm',
- className
- )}
- {...props}
- />
- )
- }
-
- export function CardHeader({
- className,
- ref,
- ...props
- }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) {
- return (
- <div
- ref={ref}
- className={cn('flex flex-col space-y-1.5 p-6', className)}
- {...props}
- />
- )
- }
-
- export function CardTitle({
- className,
- ref,
- ...props
- }: React.HTMLAttributes<HTMLHeadingElement> & { ref?: React.Ref<HTMLHeadingElement> }) {
- return (
- <h3
- ref={ref}
- className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
- {...props}
- />
- )
- }
-
- export function CardDescription({
- className,
- ref,
- ...props
- }: React.HTMLAttributes<HTMLParagraphElement> & { ref?: React.Ref<HTMLParagraphElement> }) {
- return (
- <p
- ref={ref}
- className={cn('text-sm text-muted-foreground', className)}
- {...props}
- />
- )
- }
-
- export function CardContent({
- className,
- ref,
- ...props
- }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) {
- return (
- <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
- )
- }
-
- export function CardFooter({
- className,
- ref,
- ...props
- }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) {
- return (
- <div
- ref={ref}
- className={cn('flex items-center p-6 pt-0', className)}
- {...props}
- />
- )
- }
-
- // Usage
- <Card>
- <CardHeader>
- <CardTitle>Account</CardTitle>
- <CardDescription>Manage your account settings</CardDescription>
- </CardHeader>
- <CardContent>
- <form>...</form>
- </CardContent>
- <CardFooter>
- <Button>Save</Button>
- </CardFooter>
- </Card>
- ```
-
- ### Pattern 3: Form Components
-
- ```typescript
- // components/ui/input.tsx
- import { cn } from '@/lib/utils'
-
- export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
- error?: string
- ref?: React.Ref<HTMLInputElement>
- }
-
- export function Input({ className, type, error, ref, ...props }: InputProps) {
- return (
- <div className="relative">
- <input
- type={type}
- className={cn(
- 'flex h-10 w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
- error && 'border-destructive focus-visible:ring-destructive',
- className
- )}
- ref={ref}
- aria-invalid={!!error}
- aria-describedby={error ? `${props.id}-error` : undefined}
- {...props}
- />
- {error && (
- <p
- id={`${props.id}-error`}
- className="mt-1 text-sm text-destructive"
- role="alert"
- >
- {error}
- </p>
- )}
- </div>
- )
- }
-
- // components/ui/label.tsx
- import { cva, type VariantProps } from 'class-variance-authority'
-
- const labelVariants = cva(
- 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
- )
-
- export function Label({
- className,
- ref,
- ...props
- }: React.LabelHTMLAttributes<HTMLLabelElement> & { ref?: React.Ref<HTMLLabelElement> }) {
- return (
- <label ref={ref} className={cn(labelVariants(), className)} {...props} />
- )
- }
-
- // Usage with React Hook Form + Zod
- import { useForm } from 'react-hook-form'
- import { zodResolver } from '@hookform/resolvers/zod'
- import { z } from 'zod'
-
- const schema = z.object({
- email: z.string().email('Invalid email address'),
- password: z.string().min(8, 'Password must be at least 8 characters'),
- })
-
- function LoginForm() {
- const { register, handleSubmit, formState: { errors } } = useForm({
- resolver: zodResolver(schema),
- })
-
- return (
- <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
- <div className="space-y-2">
- <Label htmlFor="email">Email</Label>
- <Input
- id="email"
- type="email"
- {...register('email')}
- error={errors.email?.message}
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="password">Password</Label>
- <Input
- id="password"
- type="password"
- {...register('password')}
- error={errors.password?.message}
- />
- </div>
- <Button type="submit" className="w-full">Sign In</Button>
- </form>
- )
- }
- ```
-
- ### Pattern 4: Responsive Grid System
-
- ```typescript
- // components/ui/grid.tsx
- import { cn } from '@/lib/utils'
- import { cva, type VariantProps } from 'class-variance-authority'
-
- const gridVariants = cva('grid', {
- variants: {
- cols: {
- 1: 'grid-cols-1',
- 2: 'grid-cols-1 sm:grid-cols-2',
- 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
- 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
- 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
- 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
- },
- gap: {
- none: 'gap-0',
- sm: 'gap-2',
- md: 'gap-4',
- lg: 'gap-6',
- xl: 'gap-8',
- },
- },
- defaultVariants: {
- cols: 3,
- gap: 'md',
- },
- })
-
- interface GridProps
- extends React.HTMLAttributes<HTMLDivElement>,
- VariantProps<typeof gridVariants> {}
-
- export function Grid({ className, cols, gap, ...props }: GridProps) {
- return (
- <div className={cn(gridVariants({ cols, gap, className }))} {...props} />
- )
- }
-
- // Container component
- const containerVariants = cva('mx-auto w-full px-4 sm:px-6 lg:px-8', {
- variants: {
- size: {
- sm: 'max-w-screen-sm',
- md: 'max-w-screen-md',
- lg: 'max-w-screen-lg',
- xl: 'max-w-screen-xl',
- '2xl': 'max-w-screen-2xl',
- full: 'max-w-full',
- },
- },
- defaultVariants: {
- size: 'xl',
- },
- })
-
- interface ContainerProps
- extends React.HTMLAttributes<HTMLDivElement>,
- VariantProps<typeof containerVariants> {}
-
- export function Container({ className, size, ...props }: ContainerProps) {
- return (
- <div className={cn(containerVariants({ size, className }))} {...props} />
- )
- }
-
- // Usage
- <Container>
- <Grid cols={4} gap="lg">
- {products.map((product) => (
- <ProductCard key={product.id} product={product} />
- ))}
- </Grid>
- </Container>
- ```
-
- For advanced animation and dark mode patterns, see [references/advanced-patterns.md](references/advanced-patterns.md):
-
- - **Pattern 5: Native CSS Animations** — dialog `@keyframes`, native popover API with `@starting-style`, `allow-discrete` transitions, and a full `DialogContent`/`DialogOverlay` implementation using Radix UI
- - **Pattern 6: Dark Mode** — `ThemeProvider` context with `localStorage` persistence, `prefers-color-scheme` detection, meta `theme-color` update, and a `ThemeToggle` button component
-
- ## Utility Functions
-
- ```typescript
- // lib/utils.ts
- import { type ClassValue, clsx } from "clsx";
- import { twMerge } from "tailwind-merge";
-
- export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
- }
-
- // Focus ring utility
- export const focusRing = cn(
- "focus-visible:outline-none focus-visible:ring-2",
- "focus-visible:ring-ring focus-visible:ring-offset-2",
- );
-
- // Disabled utility
- export const disabled = "disabled:pointer-events-none disabled:opacity-50";
- ```
+ ## Detailed patterns and worked examples
- For advanced v4 CSS patterns, the full v3-to-v4 migration checklist, and complete best practices, see [references/advanced-patterns.md](references/advanced-patterns.md):
+ Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
- - **Custom `@utility`** — reusable CSS utilities for decorative lines and text gradients
- - **Theme modifiers** — `@theme inline` (reference other CSS vars), `@theme static` (always output), `@import "tailwindcss" theme(static)`
- - **Namespace overrides** — clearing default Tailwind color scales with `--color-*: initial`
- - **Semi-transparent variants** — `color-mix()` for alpha scale generation
- - **Container queries** — `--container-*` token definitions
- - **v3→v4 migration checklist** — 10-item checklist covering config, directives, colors, dark mode, animations, React 19 ref changes
- - **Best practices** — full Do's and Don'ts list