mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 17:47:16 +00:00
669 lines
18 KiB
Markdown
669 lines
18 KiB
Markdown
---
|
|
name: tailwind-design-system
|
|
description: Build scalable design systems with Tailwind CSS, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.
|
|
---
|
|
|
|
# Tailwind Design System
|
|
|
|
Build production-ready design systems with Tailwind CSS, including design tokens, component variants, responsive patterns, and accessibility.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Creating a component library with Tailwind
|
|
- Implementing design tokens and theming
|
|
- Building responsive and accessible components
|
|
- Standardizing UI patterns across a codebase
|
|
- Migrating to or extending Tailwind CSS
|
|
- Setting up dark mode and color schemes
|
|
|
|
## Core Concepts
|
|
|
|
### 1. Design Token Hierarchy
|
|
|
|
```
|
|
Brand Tokens (abstract)
|
|
└── Semantic Tokens (purpose)
|
|
└── Component Tokens (specific)
|
|
|
|
Example:
|
|
blue-500 → primary → button-bg
|
|
```
|
|
|
|
### 2. Component Architecture
|
|
|
|
```
|
|
Base styles → Variants → Sizes → States → Overrides
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
```typescript
|
|
// tailwind.config.ts
|
|
import type { Config } from "tailwindcss";
|
|
|
|
const config: Config = {
|
|
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
|
|
darkMode: "class",
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
// Semantic color tokens
|
|
primary: {
|
|
DEFAULT: "hsl(var(--primary))",
|
|
foreground: "hsl(var(--primary-foreground))",
|
|
},
|
|
secondary: {
|
|
DEFAULT: "hsl(var(--secondary))",
|
|
foreground: "hsl(var(--secondary-foreground))",
|
|
},
|
|
destructive: {
|
|
DEFAULT: "hsl(var(--destructive))",
|
|
foreground: "hsl(var(--destructive-foreground))",
|
|
},
|
|
muted: {
|
|
DEFAULT: "hsl(var(--muted))",
|
|
foreground: "hsl(var(--muted-foreground))",
|
|
},
|
|
accent: {
|
|
DEFAULT: "hsl(var(--accent))",
|
|
foreground: "hsl(var(--accent-foreground))",
|
|
},
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
border: "hsl(var(--border))",
|
|
ring: "hsl(var(--ring))",
|
|
},
|
|
borderRadius: {
|
|
lg: "var(--radius)",
|
|
md: "calc(var(--radius) - 2px)",
|
|
sm: "calc(var(--radius) - 4px)",
|
|
},
|
|
},
|
|
},
|
|
plugins: [require("tailwindcss-animate")],
|
|
};
|
|
|
|
export default config;
|
|
```
|
|
|
|
```css
|
|
/* globals.css */
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 222.2 84% 4.9%;
|
|
--primary: 222.2 47.4% 11.2%;
|
|
--primary-foreground: 210 40% 98%;
|
|
--secondary: 210 40% 96.1%;
|
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
--muted: 210 40% 96.1%;
|
|
--muted-foreground: 215.4 16.3% 46.9%;
|
|
--accent: 210 40% 96.1%;
|
|
--accent-foreground: 222.2 47.4% 11.2%;
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
--border: 214.3 31.8% 91.4%;
|
|
--ring: 222.2 84% 4.9%;
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
.dark {
|
|
--background: 222.2 84% 4.9%;
|
|
--foreground: 210 40% 98%;
|
|
--primary: 210 40% 98%;
|
|
--primary-foreground: 222.2 47.4% 11.2%;
|
|
--secondary: 217.2 32.6% 17.5%;
|
|
--secondary-foreground: 210 40% 98%;
|
|
--muted: 217.2 32.6% 17.5%;
|
|
--muted-foreground: 215 20.2% 65.1%;
|
|
--accent: 217.2 32.6% 17.5%;
|
|
--accent-foreground: 210 40% 98%;
|
|
--destructive: 0 62.8% 30.6%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
--border: 217.2 32.6% 17.5%;
|
|
--ring: 212.7 26.8% 83.9%;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Patterns
|
|
|
|
### Pattern 1: CVA (Class Variance Authority) Components
|
|
|
|
```typescript
|
|
// components/ui/button.tsx
|
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
import { forwardRef } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const buttonVariants = cva(
|
|
// Base styles
|
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background 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-input 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: 'h-10 w-10',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: 'default',
|
|
size: 'default',
|
|
},
|
|
}
|
|
)
|
|
|
|
export interface ButtonProps
|
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
VariantProps<typeof buttonVariants> {
|
|
asChild?: boolean
|
|
}
|
|
|
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
const Comp = asChild ? Slot : 'button'
|
|
return (
|
|
<Comp
|
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
)
|
|
Button.displayName = 'Button'
|
|
|
|
export { Button, buttonVariants }
|
|
|
|
// 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
|
|
|
|
```typescript
|
|
// components/ui/card.tsx
|
|
import { cn } from '@/lib/utils'
|
|
import { forwardRef } from 'react'
|
|
|
|
const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
)
|
|
Card.displayName = 'Card'
|
|
|
|
const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
)
|
|
CardHeader.displayName = 'CardHeader'
|
|
|
|
const CardTitle = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<h3
|
|
ref={ref}
|
|
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
)
|
|
CardTitle.displayName = 'CardTitle'
|
|
|
|
const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<p
|
|
ref={ref}
|
|
className={cn('text-sm text-muted-foreground', className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
)
|
|
CardDescription.displayName = 'CardDescription'
|
|
|
|
const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
)
|
|
)
|
|
CardContent.displayName = 'CardContent'
|
|
|
|
const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn('flex items-center p-6 pt-0', className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
)
|
|
CardFooter.displayName = 'CardFooter'
|
|
|
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
|
|
|
|
// Usage
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Account</CardTitle>
|
|
<CardDescription>Manage your account settings</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form>...</form>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button>Save</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
```
|
|
|
|
### Pattern 3: Form Components
|
|
|
|
```typescript
|
|
// components/ui/input.tsx
|
|
import { forwardRef } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
error?: string
|
|
}
|
|
|
|
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
({ className, type, error, ...props }, ref) => {
|
|
return (
|
|
<div className="relative">
|
|
<input
|
|
type={type}
|
|
className={cn(
|
|
'flex h-10 w-full rounded-md border border-input 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>
|
|
)
|
|
}
|
|
)
|
|
Input.displayName = 'Input'
|
|
|
|
// 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'
|
|
)
|
|
|
|
const Label = forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
|
|
({ className, ...props }, ref) => (
|
|
<label ref={ref} className={cn(labelVariants(), className)} {...props} />
|
|
)
|
|
)
|
|
Label.displayName = 'Label'
|
|
|
|
// Usage with React Hook Form
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import * as z from 'zod'
|
|
|
|
const schema = z.object({
|
|
email: z.string().email('Invalid email address'),
|
|
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
})
|
|
|
|
function LoginForm() {
|
|
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
resolver: zodResolver(schema),
|
|
})
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
{...register('email')}
|
|
error={errors.email?.message}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">Password</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
{...register('password')}
|
|
error={errors.password?.message}
|
|
/>
|
|
</div>
|
|
<Button type="submit" className="w-full">Sign In</Button>
|
|
</form>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Pattern 4: Responsive Grid System
|
|
|
|
```typescript
|
|
// components/ui/grid.tsx
|
|
import { cn } from '@/lib/utils'
|
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
|
|
const gridVariants = cva('grid', {
|
|
variants: {
|
|
cols: {
|
|
1: 'grid-cols-1',
|
|
2: 'grid-cols-1 sm:grid-cols-2',
|
|
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
|
5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
|
|
6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
|
|
},
|
|
gap: {
|
|
none: 'gap-0',
|
|
sm: 'gap-2',
|
|
md: 'gap-4',
|
|
lg: 'gap-6',
|
|
xl: 'gap-8',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
cols: 3,
|
|
gap: 'md',
|
|
},
|
|
})
|
|
|
|
interface GridProps
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
VariantProps<typeof gridVariants> {}
|
|
|
|
export function Grid({ className, cols, gap, ...props }: GridProps) {
|
|
return (
|
|
<div className={cn(gridVariants({ cols, gap, className }))} {...props} />
|
|
)
|
|
}
|
|
|
|
// Container component
|
|
const containerVariants = cva('mx-auto w-full px-4 sm:px-6 lg:px-8', {
|
|
variants: {
|
|
size: {
|
|
sm: 'max-w-screen-sm',
|
|
md: 'max-w-screen-md',
|
|
lg: 'max-w-screen-lg',
|
|
xl: 'max-w-screen-xl',
|
|
'2xl': 'max-w-screen-2xl',
|
|
full: 'max-w-full',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'xl',
|
|
},
|
|
})
|
|
|
|
interface ContainerProps
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
VariantProps<typeof containerVariants> {}
|
|
|
|
export function Container({ className, size, ...props }: ContainerProps) {
|
|
return (
|
|
<div className={cn(containerVariants({ size, className }))} {...props} />
|
|
)
|
|
}
|
|
|
|
// Usage
|
|
<Container>
|
|
<Grid cols={4} gap="lg">
|
|
{products.map((product) => (
|
|
<ProductCard key={product.id} product={product} />
|
|
))}
|
|
</Grid>
|
|
</Container>
|
|
```
|
|
|
|
### Pattern 5: Animation Utilities
|
|
|
|
```typescript
|
|
// lib/animations.ts - Tailwind CSS Animate utilities
|
|
import { cn } from './utils'
|
|
|
|
export const fadeIn = 'animate-in fade-in duration-300'
|
|
export const fadeOut = 'animate-out fade-out duration-300'
|
|
export const slideInFromTop = 'animate-in slide-in-from-top duration-300'
|
|
export const slideInFromBottom = 'animate-in slide-in-from-bottom duration-300'
|
|
export const slideInFromLeft = 'animate-in slide-in-from-left duration-300'
|
|
export const slideInFromRight = 'animate-in slide-in-from-right duration-300'
|
|
export const zoomIn = 'animate-in zoom-in-95 duration-300'
|
|
export const zoomOut = 'animate-out zoom-out-95 duration-300'
|
|
|
|
// Compound animations
|
|
export const modalEnter = cn(fadeIn, zoomIn, 'duration-200')
|
|
export const modalExit = cn(fadeOut, zoomOut, 'duration-200')
|
|
export const dropdownEnter = cn(fadeIn, slideInFromTop, 'duration-150')
|
|
export const dropdownExit = cn(fadeOut, 'slide-out-to-top', 'duration-150')
|
|
|
|
// components/ui/dialog.tsx
|
|
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
|
|
const DialogOverlay = forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
>(({ className, ...props }, ref) => (
|
|
<DialogPrimitive.Overlay
|
|
ref={ref}
|
|
className={cn(
|
|
'fixed inset-0 z-50 bg-black/80',
|
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
|
|
const DialogContent = forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<DialogPortal>
|
|
<DialogOverlay />
|
|
<DialogPrimitive.Content
|
|
ref={ref}
|
|
className={cn(
|
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg',
|
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]',
|
|
'data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
|
'sm:rounded-lg',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</DialogPrimitive.Content>
|
|
</DialogPortal>
|
|
))
|
|
```
|
|
|
|
### Pattern 6: Dark Mode Implementation
|
|
|
|
```typescript
|
|
// providers/ThemeProvider.tsx
|
|
'use client'
|
|
|
|
import { createContext, useContext, useEffect, useState } from 'react'
|
|
|
|
type Theme = 'dark' | 'light' | 'system'
|
|
|
|
interface ThemeProviderProps {
|
|
children: React.ReactNode
|
|
defaultTheme?: Theme
|
|
storageKey?: string
|
|
}
|
|
|
|
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',
|
|
}: 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>
|
|
)
|
|
}
|
|
|
|
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="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>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Utility Functions
|
|
|
|
```typescript
|
|
// lib/utils.ts
|
|
import { type ClassValue, clsx } from "clsx";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
|
|
// Focus ring utility
|
|
export const focusRing = cn(
|
|
"focus-visible:outline-none focus-visible:ring-2",
|
|
"focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
);
|
|
|
|
// Disabled utility
|
|
export const disabled = "disabled:pointer-events-none disabled:opacity-50";
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Do's
|
|
|
|
- **Use CSS variables** - Enable runtime theming
|
|
- **Compose with CVA** - Type-safe variants
|
|
- **Use semantic colors** - `primary` not `blue-500`
|
|
- **Forward refs** - Enable composition
|
|
- **Add accessibility** - ARIA attributes, focus states
|
|
|
|
### Don'ts
|
|
|
|
- **Don't use arbitrary values** - Extend theme instead
|
|
- **Don't nest @apply** - Hurts readability
|
|
- **Don't skip focus states** - Keyboard users need them
|
|
- **Don't hardcode colors** - Use semantic tokens
|
|
- **Don't forget dark mode** - Test both themes
|
|
|
|
## Resources
|
|
|
|
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
|
- [CVA Documentation](https://cva.style/docs)
|
|
- [shadcn/ui](https://ui.shadcn.com/)
|
|
- [Radix Primitives](https://www.radix-ui.com/primitives)
|