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,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`