Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:58 +08:00
commit 925b2de0f6
21 changed files with 3934 additions and 0 deletions

View File

@@ -0,0 +1,484 @@
# Accessibility Checklist for Tailwind + shadcn/ui Setup
## Overview
This checklist ensures the Tailwind + shadcn/ui setup meets WCAG 2.1 Level AA standards and provides an inclusive user experience.
## Color & Contrast
### Requirements
- [OK] Normal text (< 18pt): Contrast ratio ≥ 4.5:1
- [OK] Large text (≥ 18pt or bold 14pt): Contrast ratio ≥ 3:1
- [OK] UI components: Contrast ratio ≥ 3:1
- [OK] Focus indicators: Contrast ratio ≥ 3:1
### Implementation
```css
/* Light mode - High contrast */
:root {
--background: 0 0% 100%; /* White */
--foreground: 222.2 84% 4.9%; /* Near black - 16.7:1 ratio */
--muted: 210 40% 96.1%; /* Light gray background */
--muted-foreground: 215.4 16.3% 46.9%; /* Medium gray text - 4.6:1 ratio */
--border: 214.3 31.8% 91.4%; /* Light border */
}
/* Dark mode - High contrast */
.dark {
--background: 222.2 84% 4.9%; /* Near black */
--foreground: 210 40% 98%; /* Near white - 16.5:1 ratio */
--muted: 217.2 32.6% 17.5%; /* Dark gray background */
--muted-foreground: 215 20.2% 65.1%; /* Light gray text - 6.8:1 ratio */
--border: 217.2 32.6% 17.5%; /* Dark border */
}
```
### Testing Contrast
```bash
# Use online tools:
# - https://webaim.org/resources/contrastchecker/
# - https://contrast-ratio.com/
# - Chrome DevTools (Lighthouse audit)
# Or programmatically:
npm install --save-dev axe-core @axe-core/playwright
```
## Focus Management
### Visible Focus Indicators
```css
/* Global focus styles */
@layer base {
*:focus-visible {
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*:focus-visible {
@apply transition-none;
}
}
}
```
### Focus Order
- [OK] Logical tab order (follows visual order)
- [OK] No keyboard traps
- [OK] Skip links for navigation
- [OK] Focus moves appropriately in modals/dialogs
### Implementation
```tsx
// Skip link (in layout)
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50"
>
Skip to main content
</a>
{/* Rest of layout */}
<main id="main-content">
{children}
</main>
```
## Keyboard Navigation
### Requirements
- [OK] All interactive elements keyboard accessible
- [OK] Logical tab order
- [OK] Keyboard shortcuts don't conflict
- [OK] Escape closes modals/dropdowns
- [OK] Arrow keys for menus/lists
- [OK] Enter/Space activates buttons
### shadcn/ui Components
All shadcn/ui components support keyboard navigation out of the box:
```tsx
// Dialog - auto-handles:
// - ESC to close
// - Focus trap
// - Return focus on close
<Dialog>
<DialogContent>
<DialogTitle>Title</DialogTitle>
{/* Content */}
</DialogContent>
</Dialog>
// Dropdown - auto-handles:
// - Arrow keys for navigation
// - Enter to select
// - ESC to close
<DropdownMenu>
<DropdownMenuTrigger>Menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
```
## Semantic HTML
### Use Proper Elements
```tsx
[OK] Good - Semantic
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>&copy; 2025 Company</p>
</footer>
[ERROR] Bad - Non-semantic
<div className="nav">
<div className="link" onClick={goHome}>Home</div>
</div>
<div className="main">
<div className="article">
<div className="title">Article Title</div>
<div>Content...</div>
</div>
</div>
```
### Heading Hierarchy
```tsx
[OK] Good - Logical hierarchy
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<h2>Another Section</h2>
[ERROR] Bad - Skips levels
<h1>Page Title</h1>
<h3>Section</h3> {/* Skipped h2 */}
<h2>Another Section</h2> {/* Out of order */}
```
## Form Accessibility
### Always Pair Labels with Inputs
```tsx
[OK] Good
<div className="space-y-2">
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" required aria-required="true" />
</div>
[ERROR] Bad
<Input placeholder="Email address" /> {/* Placeholder is not a label */}
```
### Error Messages
```tsx
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? "password-error" : undefined}
/>
{errors.password && (
<p id="password-error" className="text-sm text-destructive" role="alert">
{errors.password.message}
</p>
)}
</div>
```
### Required Fields
```tsx
<Label htmlFor="username">
Username
<span className="text-destructive" aria-label="required">*</span>
</Label>
<Input id="username" required aria-required="true" />
```
## ARIA Attributes
### When to Use ARIA
**First Rule**: Don't use ARIA unless necessary. Use semantic HTML first.
```tsx
[OK] Good - Semantic HTML (no ARIA needed)
<button>Click me</button>
[ERROR] Unnecessary ARIA
<div role="button" tabIndex={0} onClick={...}>Click me</div>
```
### Common ARIA Patterns
```tsx
// Live regions for dynamic content
<div role="status" aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Loading state
<Button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? "Loading..." : "Submit"}
</Button>
// Icon buttons need labels
<Button size="icon" aria-label="Close menu">
<X className="h-4 w-4" />
</Button>
// Expanded/collapsed state
<Button
aria-expanded={isOpen}
aria-controls="content-id"
onClick={() => setIsOpen(!isOpen)}
>
Toggle
</Button>
<div id="content-id" hidden={!isOpen}>
Content
</div>
```
## Screen Reader Support
### Visually Hidden Content
Use `.sr-only` for screen-reader-only text:
```tsx
<Button>
<span className="sr-only">Delete item</span>
<Trash className="h-4 w-4" aria-hidden="true" />
</Button>
```
### Skip Links
```tsx
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-background focus:ring-2"
>
Skip to main content
</a>
```
### Icon-Only Buttons
```tsx
// Always provide text alternative
<Button variant="ghost" size="icon" aria-label="Search">
<Search className="h-4 w-4" />
</Button>
// Or use tooltip with title
<Button variant="ghost" size="icon" title="Search">
<Search className="h-4 w-4" />
<span className="sr-only">Search</span>
</Button>
```
## Motion & Animation
### Respect User Preferences
```css
/* Disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
### Implementation in Components
```tsx
// Add to globals.css
@layer base {
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
```
## Dark Mode Accessibility
### Proper Contrast in Both Modes
```css
/* Test both themes */
:root {
/* Light mode */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Ensure ≥ 4.5:1 ratio */
}
.dark {
/* Dark mode */
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
/* Ensure ≥ 4.5:1 ratio */
}
```
### Flash Prevention
```tsx
// In app/layout.tsx
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange {/* Prevent flash */}
>
{children}
</ThemeProvider>
</body>
</html>
```
## Testing Checklist
### Manual Testing
- [ ] Navigate entire app with keyboard only (no mouse)
- [ ] Test with screen reader (NVDA, JAWS, VoiceOver)
- [ ] Zoom to 200% and ensure layout doesn't break
- [ ] Test in high contrast mode
- [ ] Verify dark mode contrast
- [ ] Check focus indicators on all interactive elements
- [ ] Test form validation with screen reader
### Automated Testing
```bash
# Install axe-core
npm install --save-dev @axe-core/playwright
# Use in tests
import { test, expect } from '@playwright/test'
import { injectAxe, checkA11y } from 'axe-playwright'
test('homepage is accessible', async ({ page }) => {
await page.goto('http://localhost:3000')
await injectAxe(page)
await checkA11y(page)
})
```
### Browser DevTools
- Chrome Lighthouse (Accessibility audit)
- Firefox Accessibility Inspector
- Edge Accessibility Insights
- axe DevTools Extension
## Common Issues & Fixes
### Issue: Missing Form Labels
```tsx
[ERROR] Problem
<Input placeholder="Email" />
[OK] Fix
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" placeholder="you@example.com" />
</div>
```
### Issue: Non-Accessible Custom Components
```tsx
[ERROR] Problem
<div onClick={handleClick} className="cursor-pointer">
Click me
</div>
[OK] Fix
<button onClick={handleClick} type="button">
Click me
</button>
```
### Issue: Low Color Contrast
```tsx
[ERROR] Problem
<p className="text-gray-400">Important text</p> {/* 2.8:1 ratio */}
[OK] Fix
<p className="text-gray-700 dark:text-gray-300">Important text</p> {/* 5.2:1 ratio */}
```
### Issue: Missing Focus Indicators
```tsx
[ERROR] Problem
<Button className="focus:outline-none">Click</Button>
[OK] Fix
<Button className="focus-visible:ring-2 focus-visible:ring-ring">Click</Button>
```
## Resources
- **WCAG Guidelines**: https://www.w3.org/WAI/WCAG21/quickref/
- **WebAIM**: https://webaim.org/
- **a11y Project**: https://www.a11yproject.com/
- **Radix UI (shadcn base)**: https://radix-ui.com (includes A11y)
- **MDN Accessibility**: https://developer.mozilla.org/en-US/docs/Web/Accessibility
## Skill Implementation
This skill ensures:
- [OK] High contrast color tokens (≥ 4.5:1 for text)
- [OK] Visible focus styles with ring utilities
- [OK] Skip link in base layout
- [OK] Semantic HTML landmarks (header, nav, main, footer)
- [OK] Proper label/input associations
- [OK] Dark mode with accessible contrast
- [OK] Reduced motion support
- [OK] All shadcn components use Radix (accessible primitives)

View File

@@ -0,0 +1,370 @@
# shadcn/ui Component Reference
## Overview
shadcn/ui provides a collection of re-usable components built with Radix UI and Tailwind CSS. Components are NOT installed as dependencies but copied into your project, giving you full control.
## Installation Command
```bash
npx shadcn-ui@latest add [component-name]
```
## Baseline Components for This Skill
The following components are installed by default:
### Core UI Elements
- **button**: Primary interactive element with variants
- **card**: Container for grouped content
- **input**: Text input field
- **label**: Form label with accessibility support
- **dialog**: Modal dialog/overlay
- **separator**: Visual or semantic separator
### Layout & Navigation
- **sheet**: Slide-over panel (mobile sidebar)
- **dropdown-menu**: Contextual menu with submenus
- **navigation-menu**: Main navigation component
### Feedback & Notifications
- **toast**: Temporary notifications (via Sonner)
- **alert**: Static notification messages
- **badge**: Status or category indicator
## Available Components by Category
### Form Components
- `checkbox` - Checkbox input with label
- `input` - Text input field
- `label` - Form label
- `textarea` - Multi-line text input
- `select` - Dropdown select
- `radio-group` - Radio button group
- `switch` - Toggle switch
- `slider` - Range slider
- `form` - Form wrapper with React Hook Form integration
- `combobox` - Searchable select (autocomplete)
- `date-picker` - Date selection with calendar
- `calendar` - Calendar component
### Layout Components
- `card` - Content card with header/footer
- `sheet` - Slide-over panel
- `dialog` - Modal dialog
- `popover` - Floating popover
- `drawer` - Bottom drawer (mobile)
- `separator` - Divider line
- `scroll-area` - Custom scrollable area
- `aspect-ratio` - Maintain aspect ratio
- `resizable` - Resizable panels
### Navigation Components
- `navigation-menu` - Main navigation
- `menubar` - Desktop menu bar
- `dropdown-menu` - Context/dropdown menu
- `context-menu` - Right-click context menu
- `tabs` - Tab navigation
- `breadcrumb` - Breadcrumb navigation
- `pagination` - Page navigation
- `command` - Command palette (⌘K)
### Feedback Components
- `toast` - Toast notifications
- `alert` - Alert messages
- `alert-dialog` - Confirmation dialog
- `badge` - Status badge
- `progress` - Progress indicator
- `skeleton` - Loading skeleton
- `spinner` - Loading spinner
### Data Display
- `table` - Data table
- `avatar` - User avatar
- `tooltip` - Hover tooltip
- `accordion` - Collapsible sections
- `collapsible` - Simple collapsible
- `hover-card` - Rich hover card
- `data-table` - Advanced data table with sorting/filtering
### Utility Components
- `toggle` - Toggle button
- `toggle-group` - Toggle button group
- `sonner` - Toast notification library integration
- `carousel` - Image/content carousel
## Usage Patterns
### Adding a Single Component
```bash
npx shadcn-ui add button
```
This creates:
- `components/ui/button.tsx`
- Updates `components.json` if needed
### Adding Multiple Components
```bash
npx shadcn-ui add button card input label
```
### Using a Component
```tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export default function Example() {
return (
<Card>
<CardHeader>
<CardTitle>Example Card</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>
)
}
```
## Component Composition Patterns
### Form with Validation
```tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from "sonner"
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export function LoginForm() {
const form = useForm({
resolver: zodResolver(schema)
})
const onSubmit = (data) => {
toast.success("Login successful!")
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" {...form.register("email")} />
</div>
<Button type="submit">Login</Button>
</form>
)
}
```
### Dialog with Form
```tsx
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function CreateDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Create New</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Item</DialogTitle>
</DialogHeader>
<form>
<Input placeholder="Item name" />
<Button type="submit">Create</Button>
</form>
</DialogContent>
</Dialog>
)
}
```
### Navigation with Dropdown
```tsx
import { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuLink } from "@/components/ui/navigation-menu"
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
export function AppNav() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink href="/">Home</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Products</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Product A</DropdownMenuItem>
<DropdownMenuItem>Product B</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)
}
```
## Customization
### Modifying Component Styles
Components use Tailwind classes and can be customized:
```tsx
// Customize colors via CSS variables in globals.css
:root {
--primary: 200 100% 50%; /* Change primary color */
}
// Or override with props
<Button className="bg-purple-600 hover:bg-purple-700">
Custom Color
</Button>
```
### Creating Variants
Use `class-variance-authority` (CVA) for variants:
```tsx
import { cva } from "class-variance-authority"
const alertVariants = cva(
"rounded-lg border p-4",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "bg-destructive text-destructive-foreground",
success: "bg-green-50 text-green-900"
}
},
defaultVariants: {
variant: "default"
}
}
)
```
## Accessibility Features
All shadcn/ui components include:
- [OK] Proper ARIA attributes
- [OK] Keyboard navigation
- [OK] Focus management
- [OK] Screen reader support
- [OK] Reduced motion support
### Example: Dialog Accessibility
```tsx
<Dialog>
{/* Auto-handled: */}
{/* - Focus trap */}
{/* - ESC to close */}
{/* - aria-labelledby */}
{/* - Backdrop click to close */}
<DialogContent>
<DialogHeader>
<DialogTitle>Title (used for aria-labelledby)</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>
```
## Best Practices
### 1. Always Use Labels with Inputs
```tsx
[OK] Good
<div>
<Label htmlFor="username">Username</Label>
<Input id="username" />
</div>
[ERROR] Bad
<Input placeholder="Username" /> {/* Placeholder is not a label */}
```
### 2. Provide Accessible Names
```tsx
[OK] Good
<Button aria-label="Close menu">
<X className="h-4 w-4" />
</Button>
[ERROR] Bad
<Button>
<X className="h-4 w-4" /> {/* No text or aria-label */}
</Button>
```
### 3. Handle Loading States
```tsx
<Button disabled={isLoading}>
{isLoading ? "Loading..." : "Submit"}
</Button>
```
### 4. Use Semantic HTML
```tsx
[OK] Good
<form onSubmit={handleSubmit}>
<Button type="submit">Submit</Button>
</form>
[ERROR] Bad
<div onClick={handleSubmit}>
<Button type="button">Submit</Button>
</div>
```
## Resources
- **Official Docs**: https://ui.shadcn.com
- **Component Examples**: https://ui.shadcn.com/examples
- **Radix UI Docs**: https://radix-ui.com
- **GitHub**: https://github.com/shadcn-ui/ui
## Integration with This Skill
The skill installs these baseline components automatically:
1. `button` - Primary actions
2. `card` - Content containers
3. `input` - Form inputs
4. `label` - Form labels
5. `dialog` - Modals
6. `separator` - Visual dividers
Additional components can be added as needed:
```bash
npx shadcn-ui add [component-name]
```

View File

@@ -0,0 +1,206 @@
# Tailwind CSS v4 Migration Guide
## Overview
Tailwind CSS v4 introduces significant changes to configuration and usage patterns. This document outlines the differences and provides migration guidance.
## Current Status (as of January 2025)
- **Tailwind CSS v3.x**: Stable, widely adopted
- **Tailwind CSS v4**: In development, alpha/beta releases available
- **Recommendation**: Use v3 with forward-compatible patterns for production projects
## Key Differences: v3 vs v4
### Configuration Format
**v3 (Current)**
```ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
darkMode: 'class',
content: ['./app/**/*.{ts,tsx}'],
theme: {
extend: {
colors: { /* ... */ }
}
},
plugins: []
} satisfies Config
```
**v4 (Future)**
```css
/* @config directive in CSS */
@config "./tailwind.config.js";
@theme {
--color-brand: #3b82f6;
--font-sans: system-ui, sans-serif;
}
```
### CSS Variables & Tokens
**v3 Pattern (Forward-Compatible)**
```css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
```
```ts
// tailwind.config.ts
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))'
}
```
**v4 Expected Pattern**
```css
@theme {
--color-background: oklch(100% 0 0);
--color-foreground: oklch(20% 0.02 270);
}
```
### Plugin System
**v3**: JavaScript-based plugins in config file
**v4**: CSS-based plugins with `@plugin` directive
## Migration Strategy
### Phase 1: v3 with Forward-Compatible Patterns (Current)
Use CSS variables extensively:
- Define semantic tokens (--primary, --background, etc.)
- Use HSL/OKLCH color space for easier manipulation
- Structure CSS variables to match expected v4 patterns
### Phase 2: v4 Alpha/Beta Testing (When Available)
Test v4 alphas in non-production projects:
```bash
npm install tailwindcss@next
```
Update configuration:
- Move color tokens to `@theme` directive
- Convert plugins to CSS-based format
- Test build performance and output
### Phase 3: v4 Stable Migration (Future)
When v4 is stable:
1. Update package: `npm install tailwindcss@latest`
2. Migrate config format (may have migration tool)
3. Test thoroughly across all components
4. Update documentation
## Recommended Patterns for This Skill
### Use CSS Custom Properties
[OK] **Good (Forward-Compatible)**
```css
:root {
--radius: 0.5rem;
--primary: 222.2 47.4% 11.2%;
}
.card {
border-radius: var(--radius);
background: hsl(var(--primary));
}
```
[ERROR] **Avoid (Hard-Coded)**
```css
.card {
border-radius: 0.5rem;
background: #1e293b;
}
```
### Semantic Color Naming
Use semantic names that describe purpose, not appearance:
- [OK] `--primary`, `--destructive`, `--muted`
- [ERROR] `--blue-500`, `--red-600`, `--gray-200`
### HSL Color Space
Use HSL (or OKLCH when v4 is available) for better color manipulation:
```css
/* HSL: Hue Saturation Lightness */
--primary: 222.2 47.4% 11.2%;
/* Usage in CSS */
background: hsl(var(--primary));
background: hsl(var(--primary) / 0.8); /* With alpha */
```
## Checking for v4 Availability
```bash
# Check installed version
npm list tailwindcss
# Check latest available version
npm view tailwindcss versions --json
# Install specific version
npm install tailwindcss@4.0.0 # When available
```
## V4-Specific Features to Watch For
### CSS-First Configuration
- `@theme` directive for token definition
- `@plugin` for extending functionality
- Native CSS nesting support
### Improved Color System
- OKLCH color space support
- Better color mixing
- Improved dark mode handling
### Performance Improvements
- Faster build times
- Smaller CSS output
- Better JIT performance
### New Utilities
- Container queries (better support)
- Cascade layers
- View transitions
## Resources
- **Tailwind CSS Docs**: https://tailwindcss.com/docs
- **V4 Alpha Docs**: https://tailwindcss.com/docs/v4-alpha
- **GitHub Discussions**: https://github.com/tailwindlabs/tailwindcss/discussions
- **Upgrade Guide**: https://tailwindcss.com/docs/upgrade-guide (when v4 is stable)
## Notes for Skill Usage
When this skill runs:
1. Check Tailwind version: `npm view tailwindcss version`
2. If v4 stable is available: Use v4 configuration patterns
3. If v4 is not available: Use v3 with forward-compatible CSS variables
4. Add comments in generated files indicating v4 migration points:
```ts
// TODO: When upgrading to Tailwind v4, move these tokens to @theme directive
```
This approach ensures the skill produces production-ready code today while being ready for v4 when it arrives.

View File

@@ -0,0 +1,548 @@
# Theme Token System
## Overview
This document describes the CSS custom property-based design token system used for Tailwind + shadcn/ui theming.
## Token Philosophy
### Semantic Naming
Tokens describe **purpose**, not appearance:
- [OK] `--primary` (describes role)
- [ERROR] `--blue-600` (describes appearance)
### Benefits
- Easy theme switching
- Consistent design system
- Automatic dark mode support
- Forward-compatible with Tailwind v4
## Color Token Structure
### HSL Format
All colors use HSL (Hue, Saturation, Lightness) format without the `hsl()` wrapper:
```css
/* Define as space-separated values */
--primary: 222.2 47.4% 11.2%;
/* Use with hsl() wrapper */
background-color: hsl(var(--primary));
/* With alpha transparency */
background-color: hsl(var(--primary) / 0.5);
```
### Why HSL?
- **Intuitive**: Easier to adjust than RGB
- **Lightness control**: Simple to create shades
- **Alpha transparency**: Works with modern CSS syntax
- **Tooling**: Better design tool support
## Core Color Tokens
### Base Colors
```css
:root {
/* Background & Foreground */
--background: 0 0% 100%; /* Pure white */
--foreground: 222.2 84% 4.9%; /* Near black text */
/* Card (elevated surfaces) */
--card: 0 0% 100%; /* White card background */
--card-foreground: 222.2 84% 4.9%; /* Card text */
/* Popover (floating UI) */
--popover: 0 0% 100%; /* White popover */
--popover-foreground: 222.2 84% 4.9%; /* Popover text */
}
```
### Semantic Colors
```css
:root {
/* Primary (brand color, main actions) */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Secondary (less prominent actions) */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted (disabled states, subtle backgrounds) */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent (highlights, hover states) */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive (errors, delete actions) */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
}
```
### UI Element Colors
```css
:root {
/* Borders */
--border: 214.3 31.8% 91.4%; /* Subtle border */
--input: 214.3 31.8% 91.4%; /* Input border */
/* Focus & Selection */
--ring: 222.2 84% 4.9%; /* Focus ring */
/* Status Colors (optional) */
--success: 142 76% 36%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
--warning-foreground: 0 0% 100%;
--info: 199 89% 48%;
--info-foreground: 0 0% 100%;
}
```
## Dark Mode Tokens
### Dark Mode Strategy
Use `.dark` class to override tokens:
```css
.dark {
/* Base */
--background: 222.2 84% 4.9%; /* Near black */
--foreground: 210 40% 98%; /* Near white */
/* Card */
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
/* Primary */
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* Secondary */
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
/* Muted */
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
/* Accent */
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
/* Destructive */
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
/* Borders */
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
/* Focus */
--ring: 212.7 26.8% 83.9%;
}
```
## Theme Presets
### Zinc (Default)
Cool, neutral gray tones. Professional and versatile.
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--border: 240 5.9% 90%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--border: 240 3.7% 15.9%;
}
```
### Slate
Slightly cooler than Zinc. Tech-focused feel.
```css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--border: 214.3 31.8% 91.4%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--border: 217.2 32.6% 17.5%;
}
```
### Neutral
True neutral grays. Clean and minimal.
```css
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--border: 0 0% 89.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--border: 0 0% 14.9%;
}
```
## Spacing & Sizing Tokens
### Border Radius
```css
:root {
--radius: 0.5rem; /* Base radius: 8px */
}
/* Usage in Tailwind config */
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
}
```
### Font Family (Optional)
```css
:root {
--font-sans: ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, 'Cascadia Code', monospace;
}
```
## Usage in Tailwind Config
### Extending Theme
```ts
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
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))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
}
}
```
## Using Tokens in Components
### In CSS
```css
.custom-component {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
}
/* With alpha */
.overlay {
background-color: hsl(var(--background) / 0.8);
}
```
### With Tailwind Utilities
```tsx
<div className="bg-background text-foreground border-border">
<h1 className="text-primary">Heading</h1>
<p className="text-muted-foreground">Subtitle</p>
<Button variant="destructive">Delete</Button>
</div>
```
## Customization Guide
### Changing Primary Color
1. **Find your HSL values**: Use a color picker that shows HSL
2. **Update light mode**:
```css
:root {
--primary: 270 80% 45%; /* Purple example */
--primary-foreground: 0 0% 100%; /* White text */
}
```
3. **Update dark mode**:
```css
.dark {
--primary: 270 80% 65%; /* Lighter purple */
--primary-foreground: 240 10% 10%; /* Dark text */
}
```
4. **Test contrast**: Use WebAIM contrast checker
### Creating Custom Tokens
```css
/* Add to globals.css */
:root {
/* Custom tokens */
--sidebar-width: 16rem;
--header-height: 4rem;
--brand-gradient: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
}
/* Use in components */
.sidebar {
width: var(--sidebar-width);
}
```
### Adding More Semantic Colors
```css
:root {
--success: 142 76% 36%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
--warning-foreground: 0 0% 100%;
}
.dark {
--success: 142 71% 45%;
--warning: 38 92% 60%;
}
```
```ts
// Add to tailwind.config.ts
colors: {
success: {
DEFAULT: 'hsl(var(--success))',
foreground: 'hsl(var(--success-foreground))'
},
warning: {
DEFAULT: 'hsl(var(--warning))',
foreground: 'hsl(var(--warning-foreground))'
}
}
```
## Best Practices
### 1. Always Pair Background/Foreground
```tsx
[OK] Good
<div className="bg-primary text-primary-foreground">
Readable text
</div>
[ERROR] Bad
<div className="bg-primary text-foreground">
Low contrast
</div>
```
### 2. Test Both Themes
Always verify colors in both light and dark mode:
```bash
# Use browser DevTools to toggle:
document.documentElement.classList.toggle('dark')
```
### 3. Use Semantic Tokens
```tsx
[OK] Good (semantic)
<p className="text-muted-foreground">Helper text</p>
<Button variant="destructive">Delete</Button>
[ERROR] Bad (hard-coded)
<p className="text-gray-500 dark:text-gray-400">Helper text</p>
<Button className="bg-red-600">Delete</Button>
```
### 4. Maintain Contrast Ratios
- Normal text: ≥ 4.5:1
- Large text: ≥ 3:1
- UI components: ≥ 3:1
## Tools for Theme Development
### Color Pickers with HSL
- [HSL Color Picker](https://hslpicker.com/)
- [Coolors](https://coolors.co/)
- [Palettte App](https://palettte.app/)
### Contrast Checkers
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Contrast Ratio](https://contrast-ratio.com/)
- Chrome DevTools (Lighthouse)
### Theme Generators
- [Realtime Colors](https://realtimecolors.com/)
- [shadcn/ui Themes](https://ui.shadcn.com/themes)
- [Tailwind Color Generator](https://uicolors.app/create)
## Example: Complete Theme
```css
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Base */
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
/* UI Elements */
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
/* Primary Brand */
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
/* Secondary Actions */
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
/* Muted Elements */
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
/* Accents */
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
/* Destructive */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
/* Borders & Inputs */
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
/* Radius */
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
```
## Resources
- **HSL Color Space**: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl
- **shadcn/ui Theming**: https://ui.shadcn.com/docs/theming
- **Tailwind CSS Customization**: https://tailwindcss.com/docs/customizing-colors
- **Design Tokens**: https://www.w3.org/community/design-tokens/