| 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. |
Repo notice (Asymmetric-al/core): This repository is Base UI only.
Shared primitives come from @base-ui/react via the shadcn base-maia
style in packages/ui. Ignore any Radix UI guidance below — never add
radix-ui/@radix-ui/* imports or dependencies; composition uses Base
UI's render prop, not asChild. See docs/ai/rules/frontend.md.
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.
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
@import "tailwindcss";
@theme {
--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% );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ;
: ;
: ;
: ;
: fade-in ease-out;
: fade-out ease-in;
: slide-in ease-out;
: slide-out ease-in;
fade-in {
{
: ;
}
{
: ;
}
}
fade-out {
{
: ;
}
{
: ;
}
}
slide-in {
{
: (-);
: ;
}
{
: ();
: ;
}
}
slide-out {
{
: ();
: ;
}
{
: (-);
: ;
}
}
}
dark (&:where(.dark, .dark *));
{
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
: ( );
}
base {
* {
border-border;
}
{
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
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'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: {
: ,
: ,
},
}
)
.<>,
<typeof buttonVariants> {
?:
}
() {
= asChild ? :
(
)
}
< variant= size=></>
Pattern 2: Compound Components (React 19)
import { cn } from '@/lib/utils'
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
}: .<> & { ref?: React.Ref<HTMLHeadingElement> }) {
(
)
}
() {
(
)
}
() {
(
)
}
() {
(
)
}
<>
</>
Pattern 3: Form Components
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 ',
&& ' ',
)}
=
=
= ? `${}` }
{}
/>
{error && (
{error}
)}
)
}
{ cva, }
labelVariants = (
)
() {
(
)
}
{ useForm }
{ zodResolver }
{ z }
schema = z.({
: z.().(),
: z.().(, ),
})
() {
{ register, handleSubmit, : { errors } } = ({
: (schema),
})
(
)
}
Pattern 4: Responsive Grid System
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 () {
(
)
}
containerVariants = (, {
: {
: {
: ,
: ,
: ,
: ,
: ,
: ,
},
},
: {
: ,
},
})
.<>,
<typeof containerVariants> {}
() {
(
)
}
<>
</>
Pattern 5: Native CSS Animations (v4)
@theme {
--animate-dialog-in: dialog-fade-in 0.2s ease-out;
--animate-dialog-out: dialog-fade-out 0.15s ease-in;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: scale(0.95) translateY(-0.5rem);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes dialog-fade-out {
from {
opacity: 1;
transform: scale(1) translateY(0);
}
to {
opacity: 0;
transform: scale(0.95) translateY(-0.5rem);
}
}
[popover] {
transition:
opacity 0.2s,
transform 0.2s,
display 0.2s allow-discrete;
opacity: 0;
transform: scale();
}
:popover-open {
: ;
: ();
}
{
:popover-open {
: ;
: ();
}
}
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { cn } from '@/lib/utils'
const DialogPortal = DialogPrimitive.Portal
export function DialogOverlay({
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
ref?: React.Ref<HTMLDivElement>
}) {
return (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80',
'data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out',
className
)}
{...props}
/>
)
}
export function DialogContent({
className,
children,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.> & {
ref?: React.Ref<HTMLDivElement>
}) {
(
)
}
Pattern 6: Dark Mode with CSS (v4)
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
resolvedTheme: 'dark' | 'light'
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme',
}: {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}) {
const [theme, setTheme] = useState<Theme>(defaultTheme)
const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light')
useEffect(() => {
const stored = localStorage.getItem(storageKey) as Theme | null
(stored) (stored)
}, [storageKey])
( {
root = .
root..(, )
resolved = theme ===
? (.(). ? : )
: theme
root..(resolved)
(resolved)
metaThemeColor = .()
(metaThemeColor) {
metaThemeColor.(, resolved === ? : )
}
}, [theme])
(
)
}
= () => {
context = ()
(!context) ()
context
}
{ , }
{ useTheme }
() {
{ resolvedTheme, setTheme } = ()
(
)
}
Utility Functions
export { cn } from "cnfast";
export type { ClassValue } from "cnfast";
export const focusRing = cn(
"focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-ring focus-visible:ring-offset-2",
);
export const disabled = "disabled:pointer-events-none disabled:opacity-50";
Advanced v4 Patterns
Custom Utilities with @utility
Define reusable custom utilities:
@utility line-t {
@apply relative before:absolute before:top-0 before:-left-[100vw] before:h-px before:w-[200vw] before:bg-gray-950/5 dark:before:bg-white/10;
}
@utility text-gradient {
@apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent;
}
Theme Modifiers
@theme inline {
--font-sans: var(--font-inter), system-ui;
}
@theme static {
--color-brand: oklch(65% 0.15 240);
}
@import "tailwindcss" theme(static);
Namespace Overrides
@theme {
--color-*: initial;
--color-white: #fff;
--color-black: #000;
--color-primary: oklch(45% 0.2 260);
--color-secondary: oklch(65% 0.15 200);
}
Semi-transparent Color Variants
@theme {
--color-primary-50: color-mix(in oklab, var(--color-primary) 5%, transparent);
--color-primary-100: color-mix(
in oklab,
var(--color-primary) 10%,
transparent
);
--color-primary-200: color-mix(
in oklab,
var(--color-primary) 20%,
transparent
);
}
Container Queries
@theme {
--container-xs: 20rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-lg: 32rem;
}
v3 to v4 Migration Checklist
Best Practices
Do's
- Use
@theme blocks - CSS-first configuration is v4's core pattern
- Use OKLCH colors - Better perceptual uniformity than HSL
- Compose with CVA - Type-safe variants
- Use semantic tokens -
bg-primary not bg-blue-500
- Use
size-* - New shorthand for w-* h-*
- Add accessibility - ARIA attributes, focus states
Don'ts
- Don't use
tailwind.config.ts - Use CSS @theme instead
- Don't use
@tailwind directives - Use @import "tailwindcss"
- Don't use
forwardRef - React 19 passes ref as prop
- Don't use arbitrary values - Extend
@theme instead
- Don't hardcode colors - Use semantic tokens
- Don't forget dark mode - Test both themes
Resources