tailwind-design-system

安装量: 21.2K
排名: #98

安装

npx skills add https://github.com/wshobson/agents --skill tailwind-design-system
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 / 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.25 rem ; --radius-md : 0.375 rem ; --radius-lg : 0.5 rem ; --radius-xl : 0.75 rem ; / Animation tokens - keyframes inside @theme are output when referenced by --animate- variables / --animate-fade-in : fade-in 0.2 s ease-out ; --animate-fade-out : fade-out 0.2 s ease-in ; --animate-slide-in : slide-in 0.3 s ease-out ; --animate-slide-out : slide-out 0.3 s 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.5 rem ) ; opacity : 0 ; } to { transform : translateY ( 0 ) ; opacity : 1 ; } } @keyframes slide-out { from { transform : translateY ( 0 ) ; opacity : 1 ; } to { transform : translateY ( -0.5 rem ) ; 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 // 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) // 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 // 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 // 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

Pattern 5: Native CSS Animations (v4) / In your CSS file - native @starting-style for entry animations / @theme { --animate-dialog-in : dialog-fade-in 0.2 s ease-out ; --animate-dialog-out : dialog-fade-out 0.15 s ease-in ; } @keyframes dialog-fade-in { from { opacity : 0 ; transform : scale ( 0.95 ) translateY ( -0.5 rem ) ; } 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.5 rem ) ; } } / Native popover animations using @starting-style / [ popover ] { transition : opacity 0.2 s , transform 0.2 s , display 0.2 s allow-discrete ; opacity : 0 ; transform : scale ( 0.95 ) ; } [ popover ] :popover-open { opacity : 1 ; transform : scale ( 1 ) ; } @starting-style { [ popover ] :popover-open { opacity : 0 ; transform : scale ( 0.95 ) ; } } // components/ui/dialog.tsx - Using native popover API 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 . Content

& { ref ? : React . Ref < HTMLDivElement

} ) { return ( < DialogPortal

< DialogOverlay /

< DialogPrimitive . Content ref = { ref } className = { cn ( 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border bg-background p-6 shadow-lg sm:rounded-lg' , 'data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out' , className ) } { ... props }

{ children } < / DialogPrimitive . Content

< / DialogPortal

) } Pattern 6: Dark Mode with CSS (v4) // providers/ThemeProvider.tsx - Simplified for 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 if ( stored ) setTheme ( stored ) } , [ storageKey ] ) useEffect ( ( ) => { const root = document . documentElement root . classList . remove ( 'light' , 'dark' ) const resolved = theme === 'system' ? ( window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? 'dark' : 'light' ) : theme root . classList . add ( resolved ) setResolvedTheme ( resolved ) // Update meta theme-color for mobile browsers const metaThemeColor = document . querySelector ( 'meta[name="theme-color"]' ) if ( metaThemeColor ) { metaThemeColor . setAttribute ( 'content' , resolved === 'dark' ? '#09090b' : '#ffffff' ) } } , [ theme ] ) return ( < ThemeContext . Provider value = { { theme , setTheme : ( newTheme ) => { localStorage . setItem ( storageKey , newTheme ) setTheme ( newTheme ) } , resolvedTheme , } }

{ children } < / ThemeContext . Provider

) } export const useTheme = ( ) => { const context = useContext ( ThemeContext ) if ( ! context ) throw new Error ( 'useTheme must be used within ThemeProvider' ) return context } // components/ThemeToggle.tsx import { Moon , Sun } from 'lucide-react' import { useTheme } from '@/providers/ThemeProvider' export function ThemeToggle ( ) { const { resolvedTheme , setTheme } = useTheme ( ) return ( < Button variant = "ghost" size = "icon" onClick = { ( ) => setTheme ( resolvedTheme === 'dark' ? 'light' : 'dark' ) }

< Sun className = "size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /

< Moon className = "absolute size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /

< span className = "sr-only"

Toggle theme < / span

< / Button

) } Utility Functions // 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" ; Advanced v4 Patterns Custom Utilities with @utility Define reusable custom utilities: / Custom utility for decorative lines / @utility line-t { @apply relative before : absolute before : top-0 before : -left-[ 100 vw ] before : h-px before : w-[ 200 vw ] before : bg-gray-950/ 5 dark : before : bg-white/ 10 ; } / Custom utility for text gradients / @utility text-gradient { @apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent ; } Theme Modifiers / Use @theme inline when referencing other CSS variables / @theme inline { --font-sans : var ( --font-inter ) , system-ui ; } / Use @theme static to always generate CSS variables (even when unused) / @theme static { --color-brand : oklch ( 65 % 0.15 240 ) ; } / Import with theme options / @import "tailwindcss" theme ( static ) ; Namespace Overrides @theme { / Clear all default colors and define your own / --color- * : initial ; --color-white :

fff

; --color-black :

000

; --color-primary : oklch ( 45 % 0.2 260 ) ; --color-secondary : oklch ( 65 % 0.15 200 ) ; / Clear ALL defaults for a minimal setup / / --: initial; / } Semi-transparent Color Variants @theme { / Use color-mix() for alpha variants / --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 : 20 rem ; --container-sm : 24 rem ; --container-md : 28 rem ; --container-lg : 32 rem ; } v3 to v4 Migration Checklist Replace tailwind.config.ts with CSS @theme block Change @tailwind base/components/utilities to @import "tailwindcss" Move color definitions to @theme { --color-: value } Replace darkMode: "class" with @custom-variant dark Move @keyframes inside @theme blocks (ensures keyframes output with theme) Replace require("tailwindcss-animate") with native CSS animations Update h-10 w-10 to size-10 (new utility) Remove forwardRef (React 19 passes ref as prop) Consider OKLCH colors for better color perception Replace custom plugins with @utility directives 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

返回排行榜