Styling with Tailwind CSS
Build accessible UIs using Tailwind CSS v4 utility classes and shadcn/ui component patterns. Tailwind v4 uses CSS-first configuration only - never create or modify tailwind.config.js/tailwind.config.ts. Supports Radix UI (default) or Base UI as primitive libraries.
Critical Rules
No tailwind.config.js - CSS-First Only
Tailwind v4 configures everything in CSS. Migrate any JS/TS config:
theme.extend.colors→@theme { --color-*: ... }plugins→@plugin "..."or@utilitycontent→@source "..."tailwindcss-animate→@import "tw-animate-css"@layer utilities→@utility name { ... }
Always Use Semantic Color Tokens
// CORRECT - respects themes and dark mode
<div className="bg-primary text-primary-foreground">
// WRONG - breaks theming
<div className="bg-blue-500 text-white">
Always pair bg-* with text-*-foreground. Extend with success/warning/info in theming.md.
Never Build Class Names Dynamically
// WRONG - breaks Tailwind scanner
<div className={`bg-${color}-500`}>
// CORRECT - complete strings via lookup
const colorMap = { red: "bg-red-500", blue: "bg-blue-500" } as const
<div className={colorMap[color]}>
cn() Merge Order
Defaults first, consumer className last (tailwind-merge last-wins):
className={cn(buttonVariants({ variant, size }), className)} // correct
className={cn(className, buttonVariants({ variant, size }))} // wrong
Animation Performance
// WRONG - transition-all causes layout thrashing
<div className="transition-all duration-300">
// CORRECT - transition only what changes
<div className="transition-colors duration-200">
// CORRECT - respect reduced motion
<div className="motion-safe:animate-fade-in">
@theme vs @theme inline
@theme- static tokens, overridable by plugins@theme inline- references CSS variables, follows dark mode changes
@theme { --color-brand: oklch(0.6 0.2 250); } /* static */
@theme inline { --color-primary: var(--primary); } /* dynamic */
See components.md for more pitfalls and theming.md for color system reference.
Core Patterns
CSS Variables for Theming
shadcn/ui uses semantic CSS variables mapped to Tailwind utilities:
/* globals.css - Light mode */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--radius: 0.5rem;
}
/* Dark mode */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
}
/* Tailwind v4: Map variables */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
}
Usage in components:
// Background colors omit the "-background" suffix
<div className="bg-primary text-primary-foreground">
<div className="bg-muted text-muted-foreground">
<div className="bg-destructive text-destructive-foreground">
Component Authoring Pattern
Components use plain functions with data-slot attributes (React 19 - no forwardRef):
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "size-9",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
// Plain function with React.ComponentProps (not forwardRef)
function Button({
className,
variant,
size,
...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
return (
<button
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
// Usage
<Button variant="outline" size="sm">Click me</Button>
Icon spacing with data-icon:
<Button>
<Spinner data-icon="inline-start" />
Generating...
</Button>
<Button>
<CheckIcon data-slot="icon" />
Save Changes
</Button>
Responsive Design
Mobile-first breakpoints:
// Stack on mobile, grid on tablet+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
// Hide on mobile
<div className="hidden md:block">
// Different layouts per breakpoint
<div className="flex flex-col md:flex-row lg:gap-8">
<aside className="w-full md:w-64">
<main className="flex-1">
</div>
// Responsive text sizes
<h1 className="text-3xl md:text-4xl lg:text-5xl">
Container Queries
First-class in Tailwind v4, no plugin needed:
<div className="@container">
<div className="grid gap-4 @sm:grid-cols-2 @lg:grid-cols-3">
Responds to container width, not viewport
</div>
</div>
// Named containers
<div className="@container/sidebar">
<nav className="hidden @md/sidebar:block">
Dark Mode
// Use dark: prefix - but prefer semantic colors over manual dark: overrides
<div className="bg-background text-foreground"> // auto dark mode
<div className="bg-white dark:bg-black"> // manual override
// Use next-themes for toggle: useTheme() → setTheme("dark" | "light")
// Always add suppressHydrationWarning to <html> to prevent flash
Common Component Patterns
Card
<div className="rounded-xl border bg-card text-card-foreground shadow">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold leading-none tracking-tight">Title</h3>
<p className="text-sm text-muted-foreground">Description</p>
</div>
<div className="p-6 pt-0">Content</div>
<div className="flex items-center p-6 pt-0">Footer</div>
</div>
Form Field
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Email
</label>
<input
type="email"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/>
<p className="text-sm text-muted-foreground">Helper text</p>
</div>
Badge
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground shadow",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive: "border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
},
},
}
)
Layout Patterns
Centered Layout
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
{/* Content */}
</div>
</div>
Sidebar Layout
<div className="flex h-screen">
<aside className="w-64 border-r bg-muted/40">Sidebar</aside>
<main className="flex-1 overflow-auto">Content</main>
</div>