Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:01:28 +08:00
commit acd2b21597
6 changed files with 1729 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "styling-with-tailwind",
"description": "Skill: Build beautiful UIs with Tailwind CSS and shadcn/ui - modern utility-first styling, component patterns, and Tailwind v4.1 features",
"version": "0.0.0-2025.11.28",
"author": {
"name": "Misha Kolesnik",
"email": "misha@kolesnik.io"
},
"skills": [
"./skills/skill"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# styling-with-tailwind
Skill: Build beautiful UIs with Tailwind CSS and shadcn/ui - modern utility-first styling, component patterns, and Tailwind v4.1 features

52
plugin.lock.json Normal file
View File

@@ -0,0 +1,52 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:tenequm/claude-plugins:styling-with-tailwind",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "437c88f75d31b608740c5301d1d22e1ab46ecfef",
"treeHash": "67fae3511f3c99a6e7a3a601c981c3849ebc66663028634f497245569c8ce1c5",
"generatedAt": "2025-11-28T10:28:38.495771Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "styling-with-tailwind",
"description": "Skill: Build beautiful UIs with Tailwind CSS and shadcn/ui - modern utility-first styling, component patterns, and Tailwind v4.1 features"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d887c0c4c600fa0a052a91981a766e83077403324aa57e012a84165a5c2e727d"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "19b9bee645a9b4dbd195f88d89f081547a22825033975bc09f418684525c4c13"
},
{
"path": "skills/skill/SKILL.md",
"sha256": "b0a2d7183f0486103ce0071dedc1ece396b07591a5edf18ceed5e4461c375128"
},
{
"path": "skills/skill/references/components.md",
"sha256": "c42d361b5d7cf0d66c67be567e1003565d291c9daa876e813014a1dc53ae0a2d"
},
{
"path": "skills/skill/references/theming.md",
"sha256": "82d1d5f0b04c0ed992489e0ef4eb0f413a154c8cc61e897ce274a4d3d7d10c31"
}
],
"dirSha256": "67fae3511f3c99a6e7a3a601c981c3849ebc66663028634f497245569c8ce1c5"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

490
skills/skill/SKILL.md Normal file
View File

@@ -0,0 +1,490 @@
---
name: styling-with-tailwind
description: Creates UIs using Tailwind CSS utility classes and shadcn/ui patterns. Covers CSS variables with OKLCH colors, component variants with CVA, responsive design, dark mode, and Tailwind v4 features. Use when building interfaces with Tailwind, styling shadcn/ui components, implementing themes, or working with utility-first CSS.
---
# Styling with Tailwind CSS
Build accessible UIs using Tailwind utility classes and shadcn/ui component patterns.
## Core Patterns
### CSS Variables for Theming
shadcn/ui uses semantic CSS variables mapped to Tailwind utilities:
```css
/* globals.css - Light mode */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--radius: 0.5rem;
}
/* Dark mode */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
}
/* Tailwind v4: Map variables */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
}
```
**Usage in components:**
```tsx
// Background colors omit the "-background" suffix
<div className="bg-primary text-primary-foreground">
<div className="bg-muted text-muted-foreground">
<div className="bg-destructive text-destructive-foreground">
```
### Component Variants with CVA
Use `class-variance-authority` for component variants:
```tsx
import { cva } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
// Usage
<Button variant="outline" size="sm">Click me</Button>
```
### Responsive Design
Mobile-first breakpoints:
```tsx
// Stack on mobile, grid on tablet+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
// Hide on mobile
<div className="hidden md:block">
// Different layouts per breakpoint
<div className="flex flex-col md:flex-row lg:gap-8">
<aside className="w-full md:w-64">
<main className="flex-1">
</div>
// Responsive text sizes
<h1 className="text-3xl md:text-4xl lg:text-5xl">
```
### Dark Mode
```tsx
// Use dark: prefix for dark mode styles
<div className="bg-white dark:bg-black text-black dark:text-white">
// Theme toggle component
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
<Sun className="rotate-0 scale-100 dark:-rotate-90 dark:scale-0" />
<Moon className="absolute rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
</button>
)
}
```
## Common Component Patterns
### Card
```tsx
<div className="rounded-xl border bg-card text-card-foreground shadow">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold leading-none tracking-tight">Title</h3>
<p className="text-sm text-muted-foreground">Description</p>
</div>
<div className="p-6 pt-0">Content</div>
<div className="flex items-center p-6 pt-0">Footer</div>
</div>
```
### Form Field
```tsx
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Email
</label>
<input
type="email"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/>
<p className="text-sm text-muted-foreground">Helper text</p>
</div>
```
### Badge
```tsx
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground shadow",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive: "border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
},
},
}
)
```
### Alert
```tsx
<div className="relative w-full rounded-lg border px-4 py-3 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11">
<AlertCircle className="size-4" />
<div className="font-medium">Alert Title</div>
<div className="text-sm text-muted-foreground">Description</div>
</div>
```
### Loading Skeleton
```tsx
<div className="space-y-2">
<div className="h-4 w-[250px] animate-pulse rounded bg-muted" />
<div className="h-4 w-[200px] animate-pulse rounded bg-muted" />
</div>
```
## Layout Patterns
### Centered Layout
```tsx
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
{/* Content */}
</div>
</div>
```
### Sidebar Layout
```tsx
<div className="flex h-screen">
<aside className="w-64 border-r bg-muted/40">Sidebar</aside>
<main className="flex-1 overflow-auto">Content</main>
</div>
```
### Dashboard Grid
```tsx
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card className="col-span-2">Wide card</Card>
<Card>Regular</Card>
<Card>Regular</Card>
<Card className="col-span-4">Full width</Card>
</div>
```
### Container with Max Width
```tsx
<div className="container mx-auto px-4 md:px-6 lg:px-8">
<div className="max-w-2xl mx-auto">
{/* Centered content */}
</div>
</div>
```
## Accessibility Patterns
### Focus Visible
```tsx
<button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
```
### Screen Reader Only
```tsx
<span className="sr-only">Close dialog</span>
```
### Disabled States
```tsx
<button className="disabled:cursor-not-allowed disabled:opacity-50" disabled>
```
### ARIA-friendly Alert
```tsx
<div role="alert" className="rounded-lg border p-4">
<div className="flex items-start gap-3">
<AlertCircle className="size-5 text-destructive" />
<div className="flex-1 space-y-1">
<h5 className="font-medium">Error</h5>
<p className="text-sm text-muted-foreground">Message</p>
</div>
</div>
</div>
```
## Tailwind v4 Features
### Size Utility
```tsx
// New syntax (replaces w-* h-*)
<div className="size-4">
<div className="size-8">
<div className="size-full">
```
### @theme Directive
```css
/* Tailwind v4 syntax */
@theme {
--color-primary: oklch(0.205 0 0);
--font-sans: "Inter", system-ui;
}
/* With CSS variables */
@theme inline {
--color-primary: var(--primary);
}
```
### Animation
```css
/* globals.css */
@import "tw-animate-css";
```
```tsx
<div className="animate-fade-in">
<div className="animate-slide-in-from-top">
<div className="animate-spin">
```
### Tailwind v4.1 Features (April 2025)
**Text Shadow:**
```tsx
// Subtle text shadows for depth
<h1 className="text-shadow-sm text-4xl font-bold">
<h2 className="text-shadow-md text-2xl">
<div className="text-shadow-lg text-xl">
// Custom text shadows
<div className="text-shadow-[0_2px_4px_rgb(0_0_0_/_0.1)]">
```
**Mask Utilities:**
```tsx
// Gradient masks for fade effects
<div className="mask-linear-to-b from-black to-transparent">
Fades to transparent at bottom
</div>
// Image masks
<div className="mask-[url('/mask.svg')]">
Masked content
</div>
// Common patterns
<div className="mask-radial-gradient">Spotlight effect</div>
```
**Colored Drop Shadow:**
```tsx
// Brand-colored shadows
<div className="drop-shadow-[0_4px_12px_oklch(0.488_0.243_264.376)]">
// Use with semantic colors
<Button className="drop-shadow-lg drop-shadow-primary/50">
Glowing button
</Button>
```
**Overflow Wrap:**
```tsx
// Break long words
<p className="overflow-wrap-anywhere">
verylongwordthatneedstowrap
</p>
<p className="overflow-wrap-break-word">
URLs and long strings
</p>
```
## OKLCH Colors
Use OKLCH for better color perception:
```css
/* Format: oklch(lightness chroma hue) */
--primary: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
/* Benefits: perceptually uniform, consistent lightness across hues */
```
## Base Color Palettes
shadcn/ui provides multiple base colors:
```css
/* Neutral (default) - pure grayscale */
--primary: oklch(0.205 0 0);
/* Zinc - cooler, blue-gray */
--primary: oklch(0.21 0.006 285.885);
/* Slate - balanced blue-gray */
--primary: oklch(0.208 0.042 265.755);
/* Stone - warmer, brown-gray */
--primary: oklch(0.216 0.006 56.043);
```
## Best Practices
### Prefer Semantic Colors
```tsx
// Good - uses theme
<div className="bg-background text-foreground">
// Avoid - hardcoded
<div className="bg-white text-black dark:bg-zinc-950">
```
### Group Related Utilities
```tsx
<div className="
flex items-center justify-between
rounded-lg border
bg-card text-card-foreground
p-4 shadow-sm
hover:bg-accent
">
```
### Avoid Arbitrary Values
```tsx
// Prefer design tokens
<div className="p-4 text-sm">
// Avoid when unnecessary
<div className="p-[17px] text-[13px]">
```
## Installation
```bash
# Initialize shadcn/ui
pnpm dlx shadcn@latest init
# Add components
pnpm dlx shadcn@latest add button card form
# Add all components
pnpm dlx shadcn@latest add --all
```
## Troubleshooting
**Colors not updating:**
1. Check CSS variable in globals.css
2. Verify @theme inline includes variable
3. Clear build cache
**Dark mode not working:**
1. Verify ThemeProvider wraps app
2. Check suppressHydrationWarning on html tag
3. Ensure dark: variants defined
**Tailwind v4 migration:**
1. Run `@tailwindcss/upgrade@next` codemod
2. Update CSS variables with hsl() wrappers
3. Change @theme to @theme inline
4. Install tw-animate-css
## Component Patterns
For detailed component patterns see [components.md](components.md):
- **Composition**: asChild pattern for wrapping elements
- **Typography**: Heading scales, prose styles, inline code
- **Forms**: React Hook Form with Zod validation
- **Icons**: Lucide icons integration and sizing
- **Inputs**: OTP, file, grouped inputs
- **Dialogs**: Modal patterns and composition
- **Data Tables**: TanStack table integration
- **Toasts**: Sonner notifications
- **CLI**: Complete command reference
## Resources
See [theming.md](theming.md) for complete color system reference and examples.
## Summary
Key concepts:
- Use semantic CSS variables for theming
- Apply CVA for component variants
- Follow mobile-first responsive patterns
- Implement dark mode with next-themes
- Use OKLCH for modern color handling
- Prefer Tailwind v4 features (size-*, @theme)
- Always ensure accessibility with focus-visible, sr-only
This skill focuses on shadcn/ui patterns with Tailwind CSS. For component-specific examples, refer to the official shadcn/ui documentation.

View File

@@ -0,0 +1,830 @@
# Component Patterns Reference
## Composition with asChild
Use `asChild` to compose components without wrapper divs:
```tsx
// Button as a Link (Next.js)
import Link from "next/link"
<Button asChild>
<Link href="/login">Login</Link>
</Button>
// Renders: <a href="/login" class="...button classes">Login</a>
// No wrapper div!
// Button as a custom component
<Button asChild variant="outline">
<a href="https://example.com" target="_blank">
External Link
</a>
</Button>
// Dialog trigger with custom element
<DialogTrigger asChild>
<div className="cursor-pointer">
Custom trigger element
</div>
</DialogTrigger>
```
**When to use:**
- Wrapping navigation links
- Custom interactive elements
- Avoiding nested buttons
- Semantic HTML (button → link when navigating)
## Typography Patterns
shadcn/ui typography scales using Tailwind utilities:
```tsx
// Headings with responsive sizing
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Taxing Laughter: The Joke Tax Chronicles
</h1>
<h2 className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0">
The People of the Kingdom
</h2>
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
The Joke Tax
</h3>
<h4 className="scroll-m-20 text-xl font-semibold tracking-tight">
People stopped telling jokes
</h4>
// Paragraph
<p className="leading-7 [&:not(:first-child)]:mt-6">
The king, seeing how much happier his subjects were, realized the error of his ways.
</p>
// Blockquote
<blockquote className="mt-6 border-l-2 pl-6 italic">
"After all," he said, "everyone enjoys a good joke."
</blockquote>
// Inline code
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
@radix-ui/react-alert-dialog
</code>
// Lead text (larger paragraph)
<p className="text-xl text-muted-foreground">
A modal dialog that interrupts the user with important content.
</p>
// Small text
<small className="text-sm font-medium leading-none">Email address</small>
// Muted text
<p className="text-sm text-muted-foreground">
Enter your email address.
</p>
// List
<ul className="my-6 ml-6 list-disc [&>li]:mt-2">
<li>1st level of puns: 5 gold coins</li>
<li>2nd level of jokes: 10 gold coins</li>
<li>3rd level of one-liners: 20 gold coins</li>
</ul>
```
## Icons with Lucide
```tsx
import { ChevronRight, Check, X, AlertCircle, Loader2 } from "lucide-react"
// Icon sizing with components
<Button>
<ChevronRight className="size-4" />
Next
</Button>
// Icons automatically adjust to button size
<Button size="sm">
<Check className="size-4" />
Small Button
</Button>
<Button size="lg">
<Check className="size-4" />
Large Button
</Button>
// Icon-only button
<Button size="icon" variant="outline">
<X className="size-4" />
</Button>
// Loading state
<Button disabled>
<Loader2 className="size-4 animate-spin" />
Please wait
</Button>
// Icon with semantic colors
<AlertCircle className="size-4 text-destructive" />
<Check className="size-4 text-green-500" />
// In alerts
<Alert>
<AlertCircle className="size-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired.
</AlertDescription>
</Alert>
```
**Icon sizing reference:**
- `size-3` - Extra small (12px)
- `size-4` - Small/default (16px)
- `size-5` - Medium (20px)
- `size-6` - Large (24px)
## Form with React Hook Form
Complete form example with validation:
```tsx
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "sonner"
// Define schema
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
bio: z.string().max(160).min(4),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
bio: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
toast.success("Profile updated successfully")
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="m@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
You can write up to 160 characters.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update profile</Button>
</form>
</Form>
)
}
```
## Input Variants
### Input OTP
```tsx
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
```
### Input with Icon
```tsx
import { Search } from "lucide-react"
<div className="relative">
<Search className="absolute left-2 top-2.5 size-4 text-muted-foreground" />
<Input placeholder="Search" className="pl-8" />
</div>
```
### File Input
```tsx
<Input
type="file"
className="cursor-pointer file:mr-4 file:rounded-md file:border-0 file:bg-primary file:px-4 file:py-2 file:text-sm file:font-semibold file:text-primary-foreground hover:file:bg-primary/90"
/>
```
### Input Group
```tsx
import { InputGroup, InputGroupText } from "@/components/ui/input-group"
<InputGroup>
<InputGroupText>https://</InputGroupText>
<Input placeholder="example.com" />
</InputGroup>
<InputGroup>
<Input placeholder="Amount" />
<InputGroupText>USD</InputGroupText>
</InputGroup>
```
## Data-Slot Composition
Components use `data-slot` attributes for styling child elements:
```tsx
// Button automatically styles icons with data-slot
<Button>
<CheckIcon data-slot="icon" />
Save Changes
</Button>
// Custom component using data-slot pattern
function CustomCard({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-lg border p-4 [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:text-muted-foreground">
{children}
</div>
)
}
// Usage
<CustomCard>
<AlertCircle data-slot="icon" />
<p>This icon is automatically styled</p>
</CustomCard>
```
**Common data-slot values:**
- `icon` - Icons within components
- `title` - Heading elements
- `description` - Descriptive text
- `action` - Action buttons or triggers
## Select Component
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="blueberry">Blueberry</SelectItem>
</SelectContent>
</Select>
// With form
<FormField
control={form.control}
name="fruit"
render={({ field }) => (
<FormItem>
<FormLabel>Fruit</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
```
## Checkbox and Radio Groups
```tsx
// Checkbox
import { Checkbox } from "@/components/ui/checkbox"
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions
</label>
</div>
// Radio Group
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
<RadioGroup defaultValue="comfortable">
<div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="r1" />
<Label htmlFor="r1">Default</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="comfortable" id="r2" />
<Label htmlFor="r2">Comfortable</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="compact" id="r3" />
<Label htmlFor="r3">Compact</Label>
</div>
</RadioGroup>
```
## Dialog Pattern
```tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit Profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
```
## Dropdown Menu
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Open</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
```
## Toast Notifications
```tsx
import { toast } from "sonner"
// Success
toast.success("Event created successfully")
// Error
toast.error("Something went wrong")
// Info
toast.info("Be aware that...")
// Warning
toast.warning("Proceed with caution")
// Loading
toast.loading("Uploading...")
// Custom
toast("Event created", {
description: "Monday, January 3rd at 6:00pm",
action: {
label: "Undo",
onClick: () => console.log("Undo"),
},
})
// Promise
toast.promise(promise, {
loading: "Loading...",
success: (data) => `${data.name} created`,
error: "Error creating event",
})
```
## Data Table Pattern
```tsx
"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
```
## CLI Commands Reference
```bash
# Initialize project
pnpm dlx shadcn@latest init
# Add specific components
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add card form input
# Add all components
pnpm dlx shadcn@latest add --all
# Update/overwrite existing components
pnpm dlx shadcn@latest add button --overwrite
pnpm dlx shadcn@latest add --all --overwrite
# Show component diff (see what changed)
pnpm dlx shadcn@latest diff button
# List available components
pnpm dlx shadcn@latest add
# Use canary release (for Tailwind v4 + React 19)
pnpm dlx shadcn@canary init
pnpm dlx shadcn@canary add button
```
## Field Component (October 2025)
Simplified form field wrapper without React Hook Form:
```tsx
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field"
<Field>
<FieldLabel>Email address</FieldLabel>
<Input
type="email"
placeholder="m@example.com"
aria-describedby="email-description email-error"
/>
<FieldDescription id="email-description">
We'll never share your email.
</FieldDescription>
<FieldError id="email-error">
{errors.email?.message}
</FieldError>
</Field>
// With validation state
<Field invalid={!!errors.password}>
<FieldLabel required>Password</FieldLabel>
<Input type="password" />
<FieldError>{errors.password?.message}</FieldError>
</Field>
// Inline field
<Field orientation="horizontal">
<FieldLabel>Subscribe</FieldLabel>
<Checkbox />
<FieldDescription>Get updates via email</FieldDescription>
</Field>
```
## Item Component (October 2025)
Flex container for list items with consistent spacing:
```tsx
import { Item, ItemIcon, ItemLabel, ItemDescription } from "@/components/ui/item"
// List item with icon
<Item>
<ItemIcon>
<FileIcon className="size-4" />
</ItemIcon>
<div>
<ItemLabel>document.pdf</ItemLabel>
<ItemDescription>2.4 MB</ItemDescription>
</div>
</Item>
// Card-style items
<Item asChild>
<a href="/dashboard" className="rounded-lg border p-4 hover:bg-accent">
<ItemIcon>
<LayoutDashboard className="size-5" />
</ItemIcon>
<div>
<ItemLabel>Dashboard</ItemLabel>
<ItemDescription>View your analytics</ItemDescription>
</div>
</a>
</Item>
// Navigation items
<nav className="space-y-1">
<Item asChild>
<a href="/home" className="px-3 py-2 rounded-md hover:bg-accent">
<ItemIcon><Home className="size-4" /></ItemIcon>
<ItemLabel>Home</ItemLabel>
</a>
</Item>
</nav>
```
## Spinner Component (October 2025)
Dedicated loading spinner component:
```tsx
import { Spinner } from "@/components/ui/spinner"
// Default spinner
<Spinner />
// With size variants
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
// In buttons
<Button disabled>
<Spinner size="sm" />
Loading...
</Button>
// Full page loading
<div className="flex min-h-screen items-center justify-center">
<Spinner size="lg" />
</div>
// With custom colors
<Spinner className="text-primary" />
```
## Button Group (October 2025)
Grouped buttons with consistent styling:
```tsx
import { ButtonGroup, ButtonGroupButton } from "@/components/ui/button-group"
// Basic button group
<ButtonGroup>
<ButtonGroupButton>Left</ButtonGroupButton>
<ButtonGroupButton>Center</ButtonGroupButton>
<ButtonGroupButton>Right</ButtonGroupButton>
</ButtonGroup>
// With active state
<ButtonGroup>
<ButtonGroupButton active>Day</ButtonGroupButton>
<ButtonGroupButton>Week</ButtonGroupButton>
<ButtonGroupButton>Month</ButtonGroupButton>
</ButtonGroup>
// With icons
<ButtonGroup>
<ButtonGroupButton>
<Bold className="size-4" />
</ButtonGroupButton>
<ButtonGroupButton>
<Italic className="size-4" />
</ButtonGroupButton>
<ButtonGroupButton>
<Underline className="size-4" />
</ButtonGroupButton>
</ButtonGroup>
// Vertical orientation
<ButtonGroup orientation="vertical">
<ButtonGroupButton>Top</ButtonGroupButton>
<ButtonGroupButton>Middle</ButtonGroupButton>
<ButtonGroupButton>Bottom</ButtonGroupButton>
</ButtonGroup>
```
## Keyboard Shortcuts Component
```tsx
import { Kbd } from "@/components/ui/kbd"
<div className="flex items-center gap-1">
<Kbd></Kbd>
<Kbd>K</Kbd>
</div>
// Search shortcut display
<div className="text-sm text-muted-foreground">
Press <Kbd></Kbd> + <Kbd>K</Kbd> to search
</div>
```
## Empty State
```tsx
import { Empty } from "@/components/ui/empty"
import { FileIcon } from "lucide-react"
<Empty>
<FileIcon className="size-10 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">No files uploaded</h3>
<p className="mb-4 mt-2 text-sm text-muted-foreground">
Upload your first file to get started
</p>
<Button>Upload File</Button>
</Empty>
```

View File

@@ -0,0 +1,342 @@
# Complete Theming Reference
## CSS Variables Structure
### Required Variables
```css
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-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-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
```
## Complete Base Color Palettes
### Neutral (Default)
```css
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
```
### Zinc (Blue-Gray)
```css
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--border: oklch(0.92 0.004 286.32);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
}
```
### Slate (Balanced Blue-Gray)
```css
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--border: oklch(0.929 0.013 255.508);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
}
```
### Stone (Warm Brown-Gray)
```css
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--border: oklch(0.923 0.003 48.717);
}
.dark {
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
}
```
### Gray (Purple-Gray)
```css
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.13 0.028 261.692);
--primary: oklch(0.21 0.034 264.665);
--primary-foreground: oklch(0.985 0.002 247.839);
--secondary: oklch(0.967 0.003 264.542);
--secondary-foreground: oklch(0.21 0.034 264.665);
--muted: oklch(0.967 0.003 264.542);
--muted-foreground: oklch(0.551 0.027 264.364);
--border: oklch(0.928 0.006 264.531);
}
.dark {
--background: oklch(0.13 0.028 261.692);
--foreground: oklch(0.985 0.002 247.839);
--primary: oklch(0.928 0.006 264.531);
--primary-foreground: oklch(0.21 0.034 264.665);
--secondary: oklch(0.278 0.033 256.848);
--secondary-foreground: oklch(0.985 0.002 247.839);
--muted: oklch(0.278 0.033 256.848);
--muted-foreground: oklch(0.707 0.022 261.325);
}
```
## Adding Custom Colors
```css
/* Add new color to root */
:root {
--warning: oklch(0.84 0.16 84);
--warning-foreground: oklch(0.28 0.07 46);
}
.dark {
--warning: oklch(0.41 0.11 46);
--warning-foreground: oklch(0.99 0.02 95);
}
/* Map to Tailwind */
@theme inline {
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}
```
Usage:
```tsx
<div className="bg-warning text-warning-foreground">
Warning message
</div>
```
## Chart Colors
```css
:root {
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
.dark {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
```
## Sidebar Colors (Optional)
```css
:root {
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.439 0 0);
}
```
## Understanding OKLCH
**Format:** `oklch(lightness chroma hue [/ alpha])`
- **Lightness** (0-1): Brightness, 0 = black, 1 = white
- **Chroma** (0-0.4): Color intensity, 0 = gray
- **Hue** (0-360): Color angle, e.g., 0/360 = red, 120 = green, 240 = blue
- **Alpha** (optional): Transparency, 0 = transparent, 1 = opaque
**Benefits:**
- Perceptually uniform (equal steps look equal)
- Consistent lightness across all hues
- Better for programmatic color manipulation
- Future-proof for modern browsers
**Examples:**
```css
/* Pure grayscale (chroma = 0) */
--background: oklch(1 0 0); /* White */
--foreground: oklch(0.145 0 0); /* Dark gray */
/* Colored (chroma > 0) */
--primary: oklch(0.577 0.245 27.325); /* Red-orange */
--accent: oklch(0.646 0.222 41.116); /* Yellow-orange */
/* With opacity */
--border: oklch(1 0 0 / 10%); /* 10% opacity white */
```
## Color Naming Convention
- **base**: Background color (no suffix)
- **base-foreground**: Text color on base background
- Always pair background/foreground for accessible contrast
Examples:
- `bg-primary` + `text-primary-foreground`
- `bg-muted` + `text-muted-foreground`
- `bg-destructive` + `text-destructive-foreground`