
Tailwind
- 146 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Apply Tailwind utility classes, responsive layouts, and design tokens while building web UI components and pages.
About
Teaches agents to build polished web interfaces with Tailwind CSS: utility classes, responsive layouts, spacing, typography, color systems, and reusable component styling patterns for SaaS and content sites.
- Utility-first CSS
- Responsive breakpoints
- Design tokens
- Component styling
- Rapid UI iteration
Tailwind by the numbers
- 146 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #959 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill tailwindAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 146 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Apply Tailwind utility classes, responsive layouts, and design tokens while building web UI components and pages.
Files
Tailwind CSS (v4+)
Overview
Tailwind CSS v4 skill covering CSS-first configuration, design tokens, component patterns, shadcn/ui integration, dark mode, container queries, migration from v3, and custom utilities.
When to use: Configuring Tailwind themes, building utility-first components, implementing dark mode, using container queries, migrating from v3, integrating shadcn/ui, troubleshooting build errors.
When NOT to use: Tailwind v3 legacy projects that will not upgrade, projects using a different styling approach (CSS Modules, styled-components) without plans to adopt Tailwind.
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| CSS-first config | @theme { --color-brand: oklch(...); } | All config in CSS, no tailwind.config.js |
| Import entry | @import "tailwindcss"; | Replaces @tailwind base/components/utilities |
| Custom utilities | @utility name { ... } | Replaces @layer utilities, works with variants |
| Functional utilities | @utility tab-* { tab-size: --value(--tab-size-*); } | Accept dynamic values via --value() |
| Plugin loading | @plugin "@tailwindcss/typography"; | Replaces require() in config |
| Container queries | @container parent + @md: child | Built-in, no plugin needed |
| Named containers | @container/sidebar + @md/sidebar: | Scope queries to specific containers |
| Dark mode variant | @custom-variant dark (&:where(.dark, .dark *)); | Class-based dark mode override |
| Theme inline | @theme inline { --color-bg: var(--bg); } | Inlines values at build, single-theme only |
| Source detection | @source "../node_modules/my-lib"; | Explicitly add scan paths |
| Reference import | @reference "../../app.css"; | Use theme in Vue/Svelte scoped styles |
| Override defaults | --color-*: initial; inside @theme | Reset a category before redefining |
| Dynamic values | grid-cols-(--my-var) | Use CSS variables in utility values |
| Text shadows | text-shadow-* | Built-in text shadow utilities |
| Starting styles | starting:opacity-0 | @starting-style variant for entry animations |
| Masks | mask-* | CSS mask utilities for image/gradient masking |
| Field sizing | field-sizing-content | Auto-sizing textareas and inputs |
| Inset shadows | inset-shadow-*, inset-ring-* | Inner shadow and ring utilities |
| User validation | user-valid:, user-invalid: | Form validation after user interaction |
| Pointer queries | pointer-fine:, pointer-coarse: | Target input device precision |
| Inert | inert:opacity-50 | Style inert elements |
| Logical spacing | pbs-*, pbe-*, mbs-*, mbe-* | Block-direction padding/margin (v4.2) |
| Logical sizing | inline-*, block-*, min-inline-*, max-block-* | Logical width/height utilities (v4.2) |
| Logical inset | inset-s-*, inset-e-*, inset-bs-*, inset-be-* | Logical positioning; replaces start-*/end-* |
| Logical borders | border-bs-*, border-be-* | Block-direction border utilities (v4.2) |
| Font features | font-features-['smcp'] | OpenType font-feature-settings (v4.2) |
| New color palettes | mauve, olive, mist, taupe | Additional neutral palettes (v4.2) |
| Webpack integration | @tailwindcss/webpack | Run Tailwind as a webpack plugin (v4.2) |
| Color space | OKLCH | Default in v4, sRGB fallbacks generated |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using tailwind.config.js in v4 | Configure via @theme { ... } in CSS |
hsl(var(--background)) double-wrap | Reference directly: var(--background) |
:root/.dark inside @layer base | Define at root level, outside any @layer |
@apply with @layer components classes | Use @utility directive for custom utilities |
@theme inline for multi-theme switching | Use @theme without inline for dynamic themes |
Raw colors like bg-blue-500 everywhere | Semantic tokens (bg-primary) that auto-adapt |
require() or @import for plugins | Use @plugin "package-name"; |
tailwindcss-animate in v4 | Use tw-animate-css instead |
Missing @theme inline with shadcn/ui | Map all CSS variables in @theme inline block |
Using theme('colors.brand') in CSS | Use var(--color-brand) native CSS variables |
Using deprecated start-*/end-* inset | Use inset-s-*/inset-e-* logical utilities |
Delegation
- Class pattern discovery and usage examples: Use
Exploreagent - v3 to v4 migration across multiple files: Use
Taskagent - Design token hierarchy and theming architecture: Use
Planagent
If the motion skill is available, delegate complex animation patterns (spring physics, gestures, scroll-linked) to it.References
- Configuration -- CSS-first config, @theme, @theme inline, @utility, @plugin, @source, @reference, @variant directives
- Design Tokens -- OKLCH token system, brand scales, semantic tokens, shadows, z-index, fluid typography
- Component Patterns -- Layouts, grids, container queries, 3D transforms, subgrid, CVA variants
- UI Patterns -- Buttons, forms, navigation, cards, typography with variants, states, accessibility
- Dark Mode -- Class-based dark mode, multi-theme systems, ThemeProvider, @custom-variant
- shadcn/ui Integration -- Four-step architecture, components.json, tw-animate-css, Vite setup
- Migration -- v3 to v4 migration steps, breaking changes, upgrade tool, common gotchas
- Troubleshooting -- Common errors, build fixes, CSS layer issues, PostCSS problems
Component Patterns
Section Container
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
{/* content */}
</section>Container width variations: max-w-4xl (narrow/blog), max-w-5xl (medium), max-w-6xl (wide), max-w-7xl (full).
Responsive Grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<Card key={item.id} {...item} />
))}
</div>Auto-Fit Grid
Columns adjust automatically based on available space:
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-6">
{/* auto columns */}
</div>Container Queries
Style elements based on their parent container size instead of viewport:
<div className="@container">
<div className="flex flex-col @md:flex-row gap-4">
<div className="w-full @md:w-1/3">Image</div>
<div className="flex-1">Content</div>
</div>
</div>Built-in container breakpoints: @xs (16rem), @sm (24rem), @md (28rem), @lg (32rem), @xl (36rem), @2xl (42rem), @3xl-@7xl.
Named Containers
Scope container queries to a specific parent:
<div className="@container/sidebar">
<nav className="@md/sidebar:flex @md/sidebar:flex-col">
{/* responds to sidebar container, not viewport */}
</nav>
</div>Max Container Queries
Apply styles when the container is at or below a breakpoint:
<div className="@container">
<div className="@max-md:flex-col @sm:@max-md:grid-cols-2">
{/* combine min and max for ranges */}
</div>
</div>Arbitrary Container Breakpoints
<div className="@container">
<div className="@[500px]:bg-red-100 @max-[800px]:text-sm">
{/* custom pixel values */}
</div>
</div>Sticky Header with Backdrop Blur
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<a href="/" className="font-bold text-xl">
Brand
</a>
<nav className="hidden md:flex gap-6">
<a href="#" className="text-sm hover:text-primary transition-colors">
Features
</a>
<a href="#" className="text-sm hover:text-primary transition-colors">
Pricing
</a>
</nav>
</div>
</div>
</header>Card Base
<div className="bg-card text-card-foreground rounded-lg border border-border p-6 hover:shadow-lg transition-shadow">
<h3 className="text-lg font-semibold mb-2">Card Title</h3>
<p className="text-muted-foreground">Card description goes here.</p>
</div>3D Transforms
Native utilities for 3D manipulation (no plugins needed):
<div className="perspective-1000">
<div className="transform-3d transition-transform duration-500 hover:rotate-y-12 hover:rotate-x-6 bg-card rounded-2xl p-8">
3D Card
</div>
</div>Key utilities: perspective-{val}, rotate-x-{val}, rotate-y-{val}, rotate-z-{val}, translate-z-{val}, scale-z-{val}, transform-3d.
Subgrid
Align nested grid items with parent columns:
<div className="grid grid-cols-4 gap-4">
<div className="col-span-3 grid grid-cols-subgrid">
<div>Aligns with parent column 1</div>
<div>Aligns with parent column 2</div>
<div>Aligns with parent column 3</div>
</div>
</div>CVA for Component Variants
Use Class Variance Authority for type-safe variant management:
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring 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-input bg-background hover:bg-accent',
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: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'default' },
},
);Usage in a component:
interface ButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}The cn() Utility
Merge Tailwind classes with conflict resolution:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Masonry-Style Layout
<div className="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6">
{items.map((item) => (
<div key={item.id} className="break-inside-avoid">
<Card {...item} />
</div>
))}
</div>Centered Content
<div className="flex flex-col items-center justify-center text-center">
<h1 className="text-4xl font-bold mb-4">Centered Title</h1>
<p className="text-muted-foreground max-w-2xl">Centered description</p>
</div>Interactive Hover Effects
{/* Lift on hover */}
<div className="transition-transform hover:scale-105">
{/* Shadow on hover */}
<div className="transition-shadow hover:shadow-lg">
{/* Color change on hover */}
<button className="transition-colors hover:bg-primary/90">Always add transition-* classes to interactive elements for smooth state changes.
Configuration
CSS-First Config (v4)
All configuration lives in CSS. No tailwind.config.js needed.
@import 'tailwindcss';
@theme {
--color-brand: oklch(0.7 0.15 250);
--color-surface-primary: oklch(1 0 0);
--color-text-primary: oklch(0.15 0.02 250);
--font-display: 'Cal Sans', 'Inter', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
--breakpoint-3xl: 1920px;
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
}@theme vs @theme inline
`@theme` generates global CSS custom properties on :root. Variables can be overridden at runtime (dark mode, multi-theme). Use for multi-theme systems.
`@theme inline` inlines values directly into generated utility classes. No CSS custom properties emitted. Use for single theme + dark mode toggle (shadcn/ui pattern).
/* Multi-theme: use @theme (keeps variable references) */
@theme {
--color-primary: var(--color-blue-500);
}
/* Single theme + shadcn: use @theme inline (inlines values) */
@theme inline {
--color-background: var(--background);
--color-primary: var(--primary);
}Overriding vs Extending the Default Theme
Adding a variable to @theme extends the defaults. To reset and replace an entire category:
@theme {
--color-*: initial; /* Reset all default colors */
--color-primary: oklch(0.58 0.2 250);
--color-secondary: oklch(0.7 0.15 200);
}The initial pattern works for any namespace: --font-*: initial;, --spacing-*: initial;, --breakpoint-*: initial;.
@utility Directive
Registers custom utility classes that work with all variants (hover:, md:, @md:).
@utility neon-text {
color: #00f0ff;
text-shadow: 0 0 5px #00f0ff;
}In v4, only @utility-defined classes work with @apply. The v3 pattern of @layer components + @apply no longer works.
Functional Utilities with --value()
Define utilities that accept dynamic values, matching against theme keys:
@theme {
--tab-size-2: 2;
--tab-size-4: 4;
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size-*);
}This enables tab-2, tab-4, tab-github classes. For arbitrary values, --value() also supports tab-[16].
@plugin Directive
Load plugins using @plugin instead of require() or @import:
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";@source Directive
Explicitly add source paths for class detection. Useful for scanning external libraries:
@source "../node_modules/@mycompany/ui/src";v4.1 additions:
/* Exclude paths from scanning */
@source not "../legacy";
/* Ensure specific classes are always generated */
@source inline("bg-red-500 text-white p-4");@reference Directive
Import a stylesheet for access to theme variables and custom utilities without duplicating CSS. For Vue/Svelte scoped styles and CSS modules:
<style>
@reference "../../app.css";
h1 {
@apply text-2xl font-bold text-red-500;
}
</style>Can also reference the framework directly:
<style>
@reference "tailwindcss";
</style>@variant Directive
Apply a Tailwind variant to custom CSS:
.my-element {
background: white;
@variant dark {
background: black;
}
}Compiles to the appropriate media query or selector based on the variant configuration.
@custom-variant Directive
Define custom variants for use in utility classes:
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant theme-midnight (&:where([data-theme="midnight"] *));Enables dark:bg-black, theme-midnight:text-white in HTML.
OKLCH Color Space (v4 Default)
v4 uses OKLCH for all default colors. Benefits: perceptual uniformity, better gradients, wider gamut. Browser support: 93%+ globally. Tailwind generates sRGB fallbacks automatically.
@theme {
--color-brand: oklch(0.7 0.15 250); /* preferred */
--color-legacy: hsl(240 80% 60%); /* still works */
}Vite Integration
Use @tailwindcss/vite instead of PostCSS for Vite projects:
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});For non-Vite environments, use @tailwindcss/postcss as the PostCSS plugin.
Webpack Integration
For webpack-based projects (Next.js pages router, Create React App, custom webpack), use @tailwindcss/webpack:
import tailwindcss from '@tailwindcss/webpack';
export default {
plugins: [tailwindcss()],
};Container Query Breakpoints
Customize container query sizes with theme variables:
@theme {
--container-8xl: 96rem;
}This creates the @8xl: container query variant for use in markup.
Dynamic CSS Variables in Utilities
Use CSS variables directly in utility class names:
<div
style={{ '--grid-count': count } as React.CSSProperties}
className="grid grid-cols-(--grid-count)"
>
{/* Dynamic grid columns */}
</div>The (--variable-name) syntax works with any utility that accepts values.
Dark Mode
Class-Based Dark Mode (v4)
Override the default prefers-color-scheme behavior with @custom-variant:
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));This enables the dark: variant to respond to a .dark class on an ancestor element (typically <html>).
Semantic Token Approach
Define CSS variables that auto-adapt, eliminating the need for dark: variants in markup:
@theme {
--color-surface: oklch(1 0 0);
--color-surface-raised: oklch(0.98 0 0);
--color-text: oklch(0.15 0 0);
--color-text-muted: oklch(0.45 0 0);
--color-border: oklch(0.9 0 0);
}
.dark {
--color-surface: oklch(0.12 0.02 260);
--color-surface-raised: oklch(0.18 0.02 260);
--color-text: oklch(0.95 0 0);
--color-text-muted: oklch(0.65 0 0);
--color-border: oklch(0.28 0.02 260);
}Usage requires no dark: prefix:
<div className="bg-surface text-text border border-border">
<p className="text-text-muted">Adapts automatically</p>
</div>ThemeProvider Component
Toggle .dark class on the document element:
'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);
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
}
function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme',
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light');
useEffect(() => {
const stored = localStorage.getItem(storageKey) as Theme | null;
if (stored) setTheme(stored);
}, [storageKey]);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
let resolved: 'dark' | 'light';
if (theme === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
} else {
resolved = theme;
}
root.classList.add(resolved);
setResolvedTheme(resolved);
}, [theme]);
const value = {
theme,
setTheme: (newTheme: Theme) => {
localStorage.setItem(storageKey, newTheme);
setTheme(newTheme);
},
resolvedTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
export { ThemeProvider, useTheme };Theme Toggle Button
import { Moon, Sun } from 'lucide-react';
import { useTheme } from '@/components/theme-provider';
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className="rounded-md p-2 hover:bg-accent transition-colors"
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</button>
);
}Media Query Dark Mode (Default)
Without @custom-variant, Tailwind uses prefers-color-scheme automatically:
@import 'tailwindcss';
/* No @custom-variant needed -- dark: uses prefers-color-scheme by default */Apply the @variant directive in custom CSS:
.my-element {
background: white;
@variant dark {
background: black;
}
}Multi-Theme Systems
For themes beyond light/dark, use @theme (without inline) and data attributes:
@import 'tailwindcss';
@custom-variant theme-ocean (&:where([data-theme="ocean"], [data-theme="ocean"] *));
@custom-variant theme-forest (&:where([data-theme="forest"], [data-theme="forest"] *));
@theme {
--color-primary: oklch(0.6 0.2 250);
--color-secondary: oklch(0.7 0.15 200);
--color-accent: oklch(0.75 0.18 30);
}
[data-theme='ocean'] {
--color-primary: oklch(0.55 0.2 220);
--color-secondary: oklch(0.65 0.15 200);
--color-accent: oklch(0.7 0.18 180);
}
[data-theme='forest'] {
--color-primary: oklch(0.5 0.18 145);
--color-secondary: oklch(0.6 0.12 120);
--color-accent: oklch(0.75 0.15 85);
}Theme Switcher
const themes = ['default', 'ocean', 'forest'] as const;
function setTheme(theme: string) {
if (theme === 'default') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
localStorage.setItem('theme', theme);
}
function initTheme() {
const saved = localStorage.getItem('theme') ?? 'default';
setTheme(saved);
}@theme inline vs @theme for Dark Mode
`@theme inline` bakes variable values at build time. Dark mode works when CSS variables change, but @theme inline has already inlined the original values. This breaks multi-theme switching.
`@theme` (without inline) keeps variable references at runtime, allowing dark mode and theme switching to work by overriding the underlying CSS variables.
| Scenario | Use |
|---|---|
| Single theme + shadcn light/dark | @theme inline works |
| Multi-theme (data attributes) | @theme required |
| Dynamic theme switching | @theme required |
Key Rules
- Define
:rootand.darkat root level, never inside@layer base - Use
hsl()oroklch()wrappers in variable definitions, not in references - Never double-wrap: use
var(--background), nothsl(var(--background)) - Semantic tokens eliminate most
dark:variants from markup - Verify
.darkclass toggles on the<html>element
Design Tokens
Token Hierarchy
Design tokens follow a three-level hierarchy:
1. Brand tokens -- Abstract color values (blue-500, brand-400) 2. Semantic tokens -- Purpose-driven names (primary, surface, error) 3. Component tokens -- Specific usage (button-bg, input-border)
Always prefer semantic names over raw values in markup.
Complete Token System
@import 'tailwindcss';
@theme {
/* Surface colors */
--color-surface-primary: oklch(1 0 0);
--color-surface-secondary: oklch(0.98 0.002 250);
--color-surface-tertiary: oklch(0.95 0.004 250);
--color-surface-inverse: oklch(0.15 0.02 250);
/* Text colors */
--color-text-primary: oklch(0.15 0.02 250);
--color-text-secondary: oklch(0.4 0.02 250);
--color-text-tertiary: oklch(0.55 0.015 250);
--color-text-inverse: oklch(0.98 0 0);
--color-text-disabled: oklch(0.7 0.01 250);
/* Border colors */
--color-border-default: oklch(0.85 0.01 250);
--color-border-subtle: oklch(0.92 0.005 250);
--color-border-strong: oklch(0.7 0.02 250);
/* Status colors */
--color-success: oklch(0.6 0.18 145);
--color-success-subtle: oklch(0.95 0.04 145);
--color-warning: oklch(0.75 0.18 85);
--color-warning-subtle: oklch(0.95 0.06 85);
--color-error: oklch(0.55 0.22 25);
--color-error-subtle: oklch(0.95 0.04 25);
--color-info: oklch(0.6 0.18 250);
--color-info-subtle: oklch(0.95 0.04 250);
}Brand Color Scale
Generate a full color scale from a brand hue using OKLCH:
@theme {
--color-brand-50: oklch(0.97 0.02 250);
--color-brand-100: oklch(0.93 0.04 250);
--color-brand-200: oklch(0.87 0.08 250);
--color-brand-300: oklch(0.78 0.12 250);
--color-brand-400: oklch(0.68 0.16 250);
--color-brand-500: oklch(0.58 0.2 250);
--color-brand-600: oklch(0.5 0.2 250);
--color-brand-700: oklch(0.42 0.18 250);
--color-brand-800: oklch(0.35 0.15 250);
--color-brand-900: oklch(0.28 0.12 250);
--color-brand-950: oklch(0.2 0.08 250);
}Replace the hue value (250) with your brand hue. OKLCH lightness ranges from 0 (black) to 1 (white).
Typography Tokens
@theme {
--font-display: 'Cal Sans', 'Inter', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
/* Type scale (Major Third ratio - 1.25) */
--text-xs: 0.64rem;
--text-sm: 0.8rem;
--text-base: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.563rem;
--text-2xl: 1.953rem;
--text-3xl: 2.441rem;
--text-4xl: 3.052rem;
--text-5xl: 3.815rem;
}Shadow Tokens with OKLCH
@theme {
--shadow-xs: 0 1px 2px 0 oklch(0 0 0 / 0.05);
--shadow-sm:
0 1px 3px 0 oklch(0 0 0 / 0.1), 0 1px 2px -1px oklch(0 0 0 / 0.1);
--shadow-md:
0 4px 6px -1px oklch(0 0 0 / 0.1), 0 2px 4px -2px oklch(0 0 0 / 0.1);
--shadow-lg:
0 10px 15px -3px oklch(0 0 0 / 0.1), 0 4px 6px -4px oklch(0 0 0 / 0.1);
--shadow-xl:
0 20px 25px -5px oklch(0 0 0 / 0.1), 0 8px 10px -6px oklch(0 0 0 / 0.1);
/* Colored shadows for branded elements */
--shadow-brand: 0 4px 14px 0 oklch(0.58 0.2 250 / 0.3);
--shadow-success: 0 4px 14px 0 oklch(0.6 0.18 145 / 0.3);
--shadow-error: 0 4px 14px 0 oklch(0.55 0.22 25 / 0.3);
}Motion Tokens
@theme {
--ease-linear: linear;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}Z-Index Scale
@theme {
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-popover: 600;
--z-tooltip: 700;
--z-toast: 800;
}Fluid Typography
Use clamp() for responsive type that scales smoothly between viewport sizes:
@theme {
--text-fluid-sm: clamp(0.8rem, 0.7rem + 0.5vw, 0.875rem);
--text-fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--text-fluid-lg: clamp(1.25rem, 1rem + 1vw, 1.5rem);
--text-fluid-xl: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
--text-fluid-2xl: clamp(2rem, 1.5rem + 2vw, 3rem);
--text-fluid-3xl: clamp(2.5rem, 1.8rem + 3vw, 4rem);
}
@utility text-fluid-lg {
font-size: var(--text-fluid-lg);
}
@utility text-fluid-xl {
font-size: var(--text-fluid-xl);
}
@utility text-fluid-2xl {
font-size: var(--text-fluid-2xl);
}Fluid Spacing
@theme {
--space-fluid-sm: clamp(0.5rem, 0.4rem + 0.5vw, 1rem);
--space-fluid-md: clamp(1rem, 0.8rem + 1vw, 2rem);
--space-fluid-lg: clamp(2rem, 1.5rem + 2vw, 4rem);
--space-fluid-xl: clamp(4rem, 3rem + 4vw, 8rem);
}
@utility p-fluid-md {
padding: var(--space-fluid-md);
}
@utility gap-fluid-md {
gap: var(--space-fluid-md);
}Monorepo Token Package
Share tokens across apps in a monorepo:
/* packages/design-tokens/tokens.css */
@theme {
--color-brand-500: oklch(0.58 0.2 250);
--color-surface-primary: oklch(1 0 0);
--font-body: 'Inter', system-ui, sans-serif;
/* all shared tokens */
}{
"name": "@mycompany/design-tokens",
"exports": { ".": "./tokens.css" }
}/* apps/web/app.css */
@import 'tailwindcss';
@import '@mycompany/design-tokens';Semantic Token Aliases
Create semantic aliases that reference base tokens:
@theme {
--color-blue-500: oklch(0.58 0.2 250);
--color-primary: var(--color-blue-500);
--color-link: var(--color-blue-500);
--color-focus-ring: var(--color-blue-500);
}This lets you change the brand color in one place and have it cascade to all semantic uses.
Migration (v3 to v4)
Automated Upgrade Tool
The official tool handles package updates, config migration, and template changes:
npx @tailwindcss/upgrade@latestWhat it does:
- Updates
tailwindcssand related dependencies to v4 - Transforms
tailwind.config.jsinto@themeblock in CSS - Migrates
@tailwinddirectives to@import "tailwindcss" - Fixes renamed or removed utilities in templates
The tool often fails with complex configurations (typography plugin configs, custom plugin setups, complex theme extensions). If it fails, follow the manual steps below.
Manual Migration Steps
1. Update the CSS Entry Point
/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* v4 */
@import 'tailwindcss';2. Move Config to CSS
/* v3: tailwind.config.js */
module.exports = {
theme: {
extend: {
colors: {
brand: '#3b82f6',
},
fontFamily: {
display: ['Clash Display', 'Inter', 'sans-serif'],
},
},
},
};/* v4: in your CSS file */
@theme {
--color-brand: #3b82f6;
--font-display: 'Clash Display', 'Inter', sans-serif;
}Then delete tailwind.config.js / tailwind.config.ts.
3. Update Plugin Syntax
/* v3 */
/* plugins: [require('@tailwindcss/typography')] in config */
/* v4 */
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";4. Replace Animation Package
pnpm remove tailwindcss-animate
pnpm add -D tw-animate-css@import 'tailwindcss';
@import 'tw-animate-css';5. Move Custom Utilities
/* v3 */
@layer utilities {
.tab-4 {
tab-size: 4;
}
}
/* v4 */
@utility tab-4 {
tab-size: 4;
}6. Move Root Variables Out of @layer
/* v3 */
@layer base {
:root {
--background: 0 0% 100%;
}
}
/* v4 */
:root {
--background: hsl(0 0% 100%);
}7. Update theme() References
/* v3 */
.my-card {
background-color: theme('colors.brand');
}
/* v4 */
.my-card {
background-color: var(--color-brand);
}Breaking Changes
CSS Layers Are Now Native
v3 hijacked the CSS @layer at-rule. v4 uses native CSS cascade layers. Impact:
@applyonly works with@utility-defined classes, not@layer componentsclasses@layer basestyles may be overridden by utility layers due to CSS cascade precedence- Define explicit layer order if using custom
@layerblocks
Default Ring Width Changed
{/* v3: ring = 3px */}
<button className="ring">
{/* v4: ring = 1px (thinner) */}
<button className="ring">
{/* Match v3 appearance */}
<button className="ring-3">Preflight Changes
v4 removes default styles for headings, lists, and buttons. All headings render at the same size. Lists lose default padding.
Fix with typography plugin:
@import 'tailwindcss';
@plugin "@tailwindcss/typography";<article className="prose dark:prose-invert">
{/* headings, lists, blockquotes styled automatically */}
</article>Or add custom base styles:
@layer base {
h1 {
font-size: var(--text-4xl);
font-weight: 700;
margin-bottom: 1rem;
}
h2 {
font-size: var(--text-3xl);
font-weight: 700;
margin-bottom: 0.75rem;
}
ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-bottom: 1rem;
}
}Color Format Changed to OKLCH
v4 replaces the entire default color palette with OKLCH. Existing hsl() values still work, but new defaults use OKLCH. Custom colors in any format are supported.
Container Queries Plugin Removed
Built into v4 core. Remove @tailwindcss/container-queries from dependencies.
Line Clamp Plugin Removed
Built into Tailwind since v3.3. Remove @tailwindcss/line-clamp if still installed.
start-*/end-* Inset Utilities Deprecated (v4.2)
Replaced by inset-s-* and inset-e-* logical inset utilities:
{/* Deprecated */}
<div className="start-0 end-4">
{/* Current */}
<div className="inset-s-0 inset-e-4">Build Tool Changes
Vite Projects
Replace PostCSS setup with the Vite plugin:
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});Remove postcss.config.js if it only contained Tailwind.
Non-Vite Projects
Use @tailwindcss/postcss (separate package from v4):
// postcss.config.js
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};The tailwindcss package itself is no longer a PostCSS plugin.
shadcn/ui Migration
1. Delete tailwind.config.ts 2. Set "config": "" in components.json 3. Replace tailwindcss-animate with tw-animate-css 4. Move :root/.dark out of @layer base 5. Add @theme inline block mapping all CSS variables 6. Wrap color values in :root with hsl() (e.g., --background: hsl(0 0% 100%)) 7. Reference variables directly in @layer base (no hsl() wrapper)
Migration Checklist
- [ ] Replace
@tailwinddirectives with@import "tailwindcss" - [ ] Move theme config from JS to
@themein CSS - [ ] Delete
tailwind.config.js/tailwind.config.ts - [ ] Update plugins to
@pluginsyntax - [ ] Replace
tailwindcss-animatewithtw-animate-css - [ ] Move
@layer utilitiesclasses to@utilitydirective - [ ] Move
:root/.darkout of@layer base - [ ] Replace
theme()withvar(--token) - [ ] Update
ringtoring-3where v3 width is expected - [ ] Check heading/list styling after Preflight changes
- [ ] Remove
@tailwindcss/container-queriesplugin - [ ] Update build tool (Vite plugin,
@tailwindcss/postcss, or@tailwindcss/webpack) - [ ] Replace
start-*/end-*withinset-s-*/inset-e-*(deprecated in v4.2)
shadcn/ui Integration
Installation
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node tw-animate-css
pnpm dlx shadcn@latest init
rm -f tailwind.config.tsVite Configuration
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: { alias: { '@': path.resolve(__dirname, './src') } },
});components.json for v4
{
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true
}
}The config field must be empty string for v4 (no tailwind.config.ts).
Four-Step Architecture
Step 1: Define CSS Variables at Root
/* src/index.css */
@import 'tailwindcss';
@import 'tw-animate-css';
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0.024 266);
--primary: oklch(0.488 0.134 262.9);
--primary-foreground: oklch(0.971 0.005 266);
--secondary: oklch(0.965 0.005 266);
--secondary-foreground: oklch(0.345 0.03 266);
--muted: oklch(0.965 0.005 266);
--muted-foreground: oklch(0.556 0.015 266);
--accent: oklch(0.965 0.005 266);
--accent-foreground: oklch(0.345 0.03 266);
--destructive: oklch(0.637 0.237 25.33);
--destructive-foreground: oklch(0.971 0.005 266);
--border: oklch(0.922 0.01 266);
--ring: oklch(0.145 0.024 266);
--radius: 0.5rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0.024 266);
}
.dark {
--background: oklch(0.145 0.024 266);
--foreground: oklch(0.971 0.005 266);
--primary: oklch(0.588 0.158 264.2);
--primary-foreground: oklch(0.345 0.03 266);
--secondary: oklch(0.269 0.022 266);
--secondary-foreground: oklch(0.971 0.005 266);
--muted: oklch(0.269 0.022 266);
--muted-foreground: oklch(0.704 0.015 266);
--accent: oklch(0.269 0.022 266);
--accent-foreground: oklch(0.971 0.005 266);
--destructive: oklch(0.444 0.177 25.33);
--destructive-foreground: oklch(0.971 0.005 266);
--border: oklch(0.269 0.022 266);
--ring: oklch(0.839 0.015 266);
--card: oklch(0.145 0.024 266);
--card-foreground: oklch(0.971 0.005 266);
}Define :root and .dark at root level using oklch values. Never inside @layer base.
Step 2: Map Variables to Tailwind Utilities
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}Without this block, utility classes like bg-background and text-primary will not exist.
Step 3: Apply Base Styles
@layer base {
body {
background-color: var(--background);
color: var(--foreground);
}
}Reference variables directly. Never double-wrap: hsl(var(--background)) is wrong.
Step 4: Automatic Dark Mode
<div className="bg-background text-foreground">
<p className="text-muted-foreground">No dark: variants needed</p>
<button className="bg-primary text-primary-foreground">
Theme switches automatically via .dark class
</button>
</div>Dark Mode Setup
Wrap the app in a ThemeProvider that toggles .dark class on <html>:
import { ThemeProvider } from '@/components/theme-provider';
ReactDOM.createRoot(document.getElementById('root')!).render(
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<App />
</ThemeProvider>,
);Add a theme toggle using shadcn's dropdown-menu:
pnpm dlx shadcn@latest add dropdown-menuThe cn() Utility
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Setup Checklist
@tailwindcss/viteinstalled and configured invite.config.tscomponents.jsonhas"config": ""- No
tailwind.config.tsfile exists src/index.cssfollows the four-step pattern:root/.darkdefined at root level (not in@layer)@theme inlinemaps all CSS variablestw-animate-cssinstalled (nottailwindcss-animate)- ThemeProvider wraps the app
Troubleshooting
bg-primary Doesn't Apply Styles
Cause: Missing @theme inline mapping.
Fix: Add the variable mapping so Tailwind generates the utility class:
@theme inline {
--color-primary: var(--primary);
}Colors All Black or White
Cause: Double hsl() wrapping.
Fix: Reference variables directly, never wrap:
/* Wrong */
background-color: hsl(var(--background));
/* Correct */
background-color: var(--background);Dark Mode Not Switching
Cause: Missing ThemeProvider or wrong class target.
Fix:
1. Wrap app in <ThemeProvider> that toggles .dark class 2. Verify .dark class toggles on <html> element (not <body>) 3. For class-based dark mode, add: @custom-variant dark (&:where(.dark, .dark *));
Build Fails with "Unexpected Config File"
Cause: v4 does not use tailwind.config.ts.
Fix: Delete the config file. All configuration goes in CSS via @theme.
rm -f tailwind.config.ts tailwind.config.js"Cannot Find Module tailwindcss-animate"
Cause: tailwindcss-animate is incompatible with v4.
Fix: Replace with v4-compatible package:
pnpm remove tailwindcss-animate
pnpm add -D tw-animate-css@import 'tailwindcss';
@import 'tw-animate-css';"Cannot Apply Unknown Utility Class"
Cause: In v4, @apply only works with @utility-defined classes.
Fix: Migrate from @layer components to @utility:
/* Wrong: v3 pattern */
@layer components {
.custom-button {
@apply px-4 py-2 bg-blue-500;
}
}
/* Correct: v4 pattern */
@utility custom-button {
@apply px-4 py-2 bg-blue-500;
}@layer base Styles Not Applying
Cause: v4 uses native CSS cascade layers. Base-layer styles have lower specificity than utility-layer styles.
Fix Option 1: Define styles at root level without @layer:
@import 'tailwindcss';
body {
background-color: var(--background);
}Fix Option 2: Import layers explicitly for correct ordering:
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/base.css' layer(base);
@import 'tailwindcss/components.css' layer(components);
@import 'tailwindcss/utilities.css' layer(utilities);
@layer base {
body {
background-color: var(--background);
}
}@theme inline Breaks Multi-Theme Dark Mode
Cause: @theme inline bakes values at build time. When dark mode changes the underlying CSS variables, utilities still reference the inlined original values.
Fix: Use @theme (without inline) for multi-theme:
@theme {
--color-text-primary: var(--color-slate-900);
--color-bg-primary: var(--color-white);
}
.dark {
--color-text-primary: var(--color-white);
--color-bg-primary: var(--color-slate-900);
}When to use `@theme inline`: Single theme + light/dark toggle (shadcn/ui default).
When to use `@theme`: Multi-theme systems, dynamic theme switching.
Ring Width Thinner Than v3
Cause: Default ring width changed from 3px to 1px in v4.
Fix: Use ring-3 to match v3 appearance:
<button className="ring-3">Match v3 ring width</button>Headings and Lists Unstyled After Migration
Cause: v4 removed default element styles from Preflight. All headings render at the same size, lists lose padding.
Fix: Use the typography plugin or add custom base styles:
@plugin "@tailwindcss/typography";<article className="prose dark:prose-invert">
{/* All elements styled automatically */}
</article>PostCSS Plugin Errors
Error: "It looks like you're trying to use tailwindcss directly as a PostCSS plugin"
Cause: v4's PostCSS plugin is a separate package.
Fix for Vite projects: Use the Vite plugin instead:
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});Fix for non-Vite projects: Install the PostCSS package:
pnpm add -D @tailwindcss/postcss// postcss.config.js
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};Duplicate @layer base Error
Cause: Multiple @layer base blocks in CSS (shadcn init may add one).
Fix: Consolidate to a single @layer base block. Keep :root/.dark variables outside any layer.
@apply Not Working in Vue/Svelte Scoped Styles
Cause: Scoped <style> blocks do not have access to theme variables.
Fix: Use @reference to import definitions:
<style>
@reference '../../app.css';
h1 {
@apply text-2xl font-bold text-red-500;
}
</style>Classes from External Library Not Detected
Cause: Tailwind ignores dependencies listed in .gitignore by default.
Fix: Add the source path explicitly:
@source '../node_modules/@mycompany/ui/src';Migration Tool Fails
The @tailwindcss/upgrade utility often fails with complex configurations.
Workaround: Follow manual migration steps. Key changes:
1. Replace @tailwind directives with @import "tailwindcss" 2. Move theme to @theme in CSS, delete config file 3. Replace tailwindcss-animate with tw-animate-css 4. Update plugins: require() to @plugin 5. Move :root/.dark out of @layer base 6. Replace @layer utilities with @utility
UI Patterns
Buttons
Base button with accessibility built-in:
<button className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50">
Button
</button>Key: focus-visible:ring-2 (keyboard), disabled:pointer-events-none (prevent clicks), transition-colors (smooth states).
Variants
<button className="bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md">Primary</button>
<button className="bg-secondary text-secondary-foreground hover:bg-secondary/80 px-4 py-2 rounded-md">Secondary</button>
<button className="border border-input bg-background hover:bg-accent px-4 py-2 rounded-md">Outline</button>
<button className="hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-md">Ghost</button>
<button className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-4 py-2 rounded-md">Delete</button>
<button className="text-primary underline-offset-4 hover:underline">Link</button>Sizes
<button className="h-9 px-3 text-sm">Small</button>
<button className="h-10 px-4 py-2">Default</button>
<button className="h-11 px-8">Large</button>
<button className="h-10 w-10" aria-label="Settings"><svg className="h-5 w-5">⚙</svg></button>Minimum touch target: 44x44px (h-11/w-11). Use aria-label for icon-only buttons.
States
<button disabled className="disabled:pointer-events-none disabled:opacity-50">Disabled</button>
<button disabled className="flex items-center gap-2"><svg className="h-4 w-4 animate-spin">⟳</svg> Loading</button>
<button className="flex items-center gap-2"><svg className="h-4 w-4">📥</svg> Download</button>Form Inputs
Text Input
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium">
Name
</label>
<input
id="name"
type="text"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Enter your name"
/>
</div>Always pair inputs with <label> using htmlFor/id. Use focus-visible:ring-2 for keyboard focus.
Input Types
<input type="email" placeholder="you@example.com" className="..." />
<input type="password" className="..." />
<div className="relative">
<input type="search" placeholder="Search..." className="pl-10 ..." />
<svg className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground">🔍</svg>
</div>Textarea and Select
<textarea rows={4} className="flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
<select className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<option>Select option</option>
</select>Checkbox and Radio
<div className="flex items-center space-x-2">
<input id="terms" type="checkbox" className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2" />
<label htmlFor="terms" className="text-sm">I agree</label>
</div>
<fieldset className="space-y-3">
<legend className="text-sm font-medium mb-2">Plan</legend>
<div className="flex items-center space-x-2">
<input id="free" type="radio" name="plan" className="h-4 w-4 border-border text-primary focus:ring-2 focus:ring-primary" />
<label htmlFor="free" className="text-sm">Free</label>
</div>
</fieldset>Minimum size: 16x16px (h-4/w-4). Use <fieldset> and <legend> for grouped controls.
Validation States
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">Email</label>
<input
id="email"
type="email"
className="border border-destructive focus-visible:ring-destructive ..."
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" className="text-sm text-destructive">Invalid email</p>
</div>
<div className="space-y-2">
<input className="border border-success focus-visible:ring-success ..." />
<p className="text-sm text-success flex items-center gap-1"><svg className="h-4 w-4">✓</svg> Available</p>
</div>Use aria-invalid and aria-describedby for screen readers.
Navigation
Sticky Header
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<a href="/" className="font-bold text-xl">
Brand
</a>
<nav className="hidden md:flex gap-6">
<a
href="#features"
className="text-sm hover:text-primary transition-colors"
>
Features
</a>
</nav>
</div>
</div>
</header>z-50 keeps header above content. backdrop-blur creates glass effect.
Mobile Menu
'use client';
import { useState } from 'react';
export function MobileNav() {
const [open, setOpen] = useState(false);
return (
<>
<button
onClick={() => setOpen(!open)}
className="md:hidden p-2"
aria-label="Toggle menu"
aria-expanded={open}
>
<svg className="h-6 w-6">{open ? '✕' : '☰'}</svg>
</button>
{open && (
<div className="md:hidden border-t border-border">
<nav className="px-4 py-6 space-y-4">
<a href="#features" className="block text-sm hover:text-primary">
Features
</a>
</nav>
</div>
)}
</>
);
}Use aria-expanded to communicate menu state.
Sidebar
<aside className="w-64 border-r border-border bg-card p-6 h-screen sticky top-0">
<nav className="space-y-1">
<a
href="/dashboard"
className="flex items-center gap-3 px-3 py-2 rounded-md bg-primary text-primary-foreground"
aria-current="page"
>
<svg className="h-5 w-5">📊</svg>
<span className="text-sm font-medium">Dashboard</span>
</a>
<a
href="/projects"
className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-muted"
>
<svg className="h-5 w-5">📁</svg>
<span className="text-sm">Projects</span>
</a>
</nav>
</aside>Use aria-current="page" for active items.
Breadcrumbs
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-2 text-sm text-muted-foreground">
<li>
<a href="/" className="hover:text-primary">
Home
</a>
</li>
<li aria-hidden="true">
<svg className="h-4 w-4">›</svg>
</li>
<li>
<a href="/blog" className="hover:text-primary">
Blog
</a>
</li>
<li aria-hidden="true">
<svg className="h-4 w-4">›</svg>
</li>
<li aria-current="page" className="font-medium text-foreground">
Article
</li>
</ol>
</nav>Tabs
<div className="border-b border-border">
<nav className="-mb-px flex gap-8" role="tablist">
<button className="border-b-2 border-primary text-sm font-medium py-4" aria-selected="true" role="tab">Overview</button>
<button className="border-b-2 border-transparent text-sm text-muted-foreground py-4" role="tab">Details</button>
</nav>
</div>
<nav className="inline-flex gap-2 p-1 bg-muted rounded-lg" role="tablist">
<button className="px-4 py-2 text-sm font-medium bg-background rounded-md shadow" role="tab">Overview</button>
<button className="px-4 py-2 text-sm text-muted-foreground" role="tab">Details</button>
</nav>Pagination
<nav className="flex items-center gap-2" aria-label="Pagination">
<button
className="px-3 py-2 rounded-md border border-border hover:bg-muted disabled:opacity-50"
disabled
>
Previous
</button>
<button
className="px-3 py-2 rounded-md bg-primary text-primary-foreground"
aria-current="page"
>
1
</button>
<button className="px-3 py-2 rounded-md border border-border hover:bg-muted">
2
</button>
<span className="px-3 py-2" aria-hidden="true">
...
</span>
<button className="px-3 py-2 rounded-md border border-border hover:bg-muted">
10
</button>
<button className="px-3 py-2 rounded-md border border-border hover:bg-muted">
Next
</button>
</nav>Cards
Base and Interactive
<div className="bg-card text-card-foreground rounded-lg border border-border p-6">
<h3 className="text-lg font-semibold mb-2">Card Title</h3>
<p className="text-muted-foreground text-sm">Description</p>
</div>
<a href="/details" className="block bg-card rounded-lg border border-border p-6 transition-all hover:shadow-lg hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<h3 className="text-lg font-semibold mb-2">Clickable Card</h3>
<p className="text-muted-foreground text-sm">Click to view</p>
</a>Use <a> for navigation, <button> for actions.
Card with Image
<div className="bg-card rounded-lg border border-border overflow-hidden">
<img src="/image.jpg" alt="Card image" className="w-full h-48 object-cover" />
<div className="p-6">
<h3 className="text-lg font-semibold mb-2">Title</h3>
<p className="text-muted-foreground text-sm">Description</p>
</div>
</div>Use overflow-hidden to preserve rounded corners when images touch edges.
Card Grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<div key={item.id} className="bg-card rounded-lg border border-border p-6">
<h3 className="text-lg font-semibold">{item.title}</h3>
</div>
))}
</div>Typography
Heading Hierarchy
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight">Page Title</h1>
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">Section</h2>
<h3 className="text-2xl font-semibold tracking-tight">Subsection</h3>
<h4 className="text-xl font-semibold">Component Title</h4>Use tracking-tight for large headings.
Body Text
<p className="text-lg text-muted-foreground leading-relaxed">Intro paragraph</p>
<p className="text-base text-muted-foreground">Body text</p>
<p className="text-sm text-muted-foreground">Caption text</p>Prose (Long-form Content)
<article className="prose prose-slate dark:prose-invert max-w-none">
<h1>Article Title</h1>
<p>Content with automatic typography...</p>
</article>Install: pnpm add @tailwindcss/typography, load with @plugin "@tailwindcss/typography";.
Code
<code className="px-1.5 py-0.5 bg-muted rounded text-sm font-mono">className</code>
<pre className="bg-muted p-4 rounded-lg overflow-x-auto">
<code className="text-sm font-mono">{`function hello() {
console.log("Hello");
}`}</code>
</pre>Accessibility
Focus Management
Use focus-visible for keyboard-only focus:
<button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
Button
</button>Never remove focus without replacement. focus-visible shows for keyboard, not mouse.
Touch Targets
Minimum sizes:
- Buttons: 44x44px (h-11/w-11)
- Checkboxes/radios: 16x16px (h-4/w-4)
- Add padding to small icons to increase touch area
ARIA Patterns
<button aria-label="Close"><svg className="h-4 w-4">✕</svg></button>
<input aria-invalid="true" aria-describedby="error-id" />
<p id="error-id">Error message</p>
<button aria-expanded={open}>Menu</button>
<a href="/page" aria-current="page">Page</a>
<button role="tab" aria-selected="true">Tab 1</button>Semantic HTML
Use proper elements:
<button>for actions<a>for navigation<label>for form inputs<fieldset>+<legend>for grouped controls<nav>for navigation- Never use
<div>or<span>for interactive elements
Common Patterns
Button Group
<div className="inline-flex rounded-md shadow-xs">
<button className="px-4 py-2 bg-primary text-primary-foreground rounded-l-md">
Left
</button>
<button className="px-4 py-2 bg-primary text-primary-foreground border-l border-primary-foreground/20">
Middle
</button>
<button className="px-4 py-2 bg-primary text-primary-foreground border-l border-primary-foreground/20 rounded-r-md">
Right
</button>
</div>Status Badge
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">Active</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning">Pending</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive">Error</span>Loading Skeleton
<div className="bg-card rounded-lg border border-border p-6 animate-pulse">
<div className="h-12 w-12 bg-muted rounded-lg mb-4" />
<div className="h-4 bg-muted rounded w-3/4 mb-2" />
<div className="h-3 bg-muted rounded w-full mb-1" />
<div className="h-3 bg-muted rounded w-5/6" />
</div>Hover Effects
<div className="transition-transform hover:scale-105">Lift</div>
<div className="transition-shadow hover:shadow-lg">Shadow</div>
<div className="transition-colors hover:border-primary">Border</div>
<div className="transition-all hover:shadow-lg hover:border-primary hover:-translate-y-1">Combined</div>Always add transition-* for smooth state changes.