ui-ux-consultant-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/ui-ux-consultant/SKILL.md +844 -0
- package/assets/ui-ux-consultant/references/accessibility.md +175 -0
- package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
- package/assets/ui-ux-consultant/references/animations.md +448 -0
- package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
- package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
- package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
- package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
- package/assets/ui-ux-consultant/references/components.md +1116 -0
- package/assets/ui-ux-consultant/references/patterns.md +600 -0
- package/assets/ui-ux-consultant/references/performance.md +198 -0
- package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
- package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
- package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
- package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
- package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
- package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
- package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
- package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
- package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
- package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
- package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
- package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
- package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
- package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
- package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
- package/assets/ui-ux-consultant/references/theming.md +701 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/package.json +51 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# shadcn/ui — UI/UX Reference
|
|
2
|
+
|
|
3
|
+
## When to Read
|
|
4
|
+
Use this file when building with shadcn/ui — the copy-paste component system built on Radix UI primitives + Tailwind CSS. Works with React, Next.js, Astro, Remix, and any Tailwind-compatible React project.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What shadcn/ui Is (and Isn't)
|
|
9
|
+
|
|
10
|
+
**NOT a component library you install as a package.** You copy component source directly into your project (`components/ui/`). Each component is yours to read, modify, and own. The `npx shadcn` CLI adds components as files — you have full control.
|
|
11
|
+
|
|
12
|
+
**Built on:**
|
|
13
|
+
- **Radix UI** — headless, accessible primitives (dialogs, dropdowns, tooltips, etc.)
|
|
14
|
+
- **Tailwind CSS** — utility classes for all styling
|
|
15
|
+
- **class-variance-authority (cva)** — variant-based component styling
|
|
16
|
+
- **clsx + tailwind-merge** — safe class name merging via `cn()`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Recommended Additions
|
|
21
|
+
|
|
22
|
+
| Package | Purpose | Install |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `react-hook-form` | Form state management | `npm install react-hook-form` |
|
|
25
|
+
| `zod` | Schema validation | `npm install zod` |
|
|
26
|
+
| `@hookform/resolvers` | Connect zod to react-hook-form | `npm install @hookform/resolvers` |
|
|
27
|
+
| `@tanstack/react-table` | Headless data tables | `npm install @tanstack/react-table` |
|
|
28
|
+
| `cmdk` | Command palette | bundled with shadcn Command |
|
|
29
|
+
| `sonner` | Toast notifications | `npx shadcn@latest add sonner` |
|
|
30
|
+
| `vaul` | Drawer / bottom sheet | `npx shadcn@latest add drawer` |
|
|
31
|
+
| `recharts` | Charts | `npx shadcn@latest add chart` |
|
|
32
|
+
| `date-fns` | Date utilities | `npm install date-fns` |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Style Recommendations
|
|
37
|
+
|
|
38
|
+
- **Default shadcn style:** Clean, professional, neutral — safe for B2B SaaS
|
|
39
|
+
- **New York style:** Sharper borders, tighter spacing — modern editorial feel
|
|
40
|
+
- **Brand customization:** Edit `--primary` CSS variable; all components update
|
|
41
|
+
- **Dark mode:** Built-in via `.dark` class on `<html>`; works out of the box
|
|
42
|
+
- **Recommended pairings:** Inter or Geist font, 4px border radius for friendly, 0px for stark
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Setup
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Initialize shadcn in a Vite/Next.js/Astro project
|
|
50
|
+
npx shadcn@latest init
|
|
51
|
+
# Prompts: style (Default / New York), base color, CSS variables (yes)
|
|
52
|
+
|
|
53
|
+
# Add individual components
|
|
54
|
+
npx shadcn@latest add button
|
|
55
|
+
npx shadcn@latest add card dialog form table badge avatar
|
|
56
|
+
|
|
57
|
+
# Add multiple at once
|
|
58
|
+
npx shadcn@latest add button card dialog form input label select textarea
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Core Architecture Patterns
|
|
64
|
+
|
|
65
|
+
### The `cn()` Helper
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// lib/utils.ts (auto-generated by init)
|
|
69
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
70
|
+
import { twMerge } from 'tailwind-merge';
|
|
71
|
+
|
|
72
|
+
export function cn(...inputs: ClassValue[]) {
|
|
73
|
+
return twMerge(clsx(inputs));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Usage — merges classes safely, resolves Tailwind conflicts
|
|
77
|
+
<Button className={cn('w-full', isLoading && 'opacity-50', className)} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Variant-Based Styling with `cva`
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// components/ui/button.tsx
|
|
84
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
85
|
+
|
|
86
|
+
const buttonVariants = cva(
|
|
87
|
+
// Base classes (always applied)
|
|
88
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
89
|
+
{
|
|
90
|
+
variants: {
|
|
91
|
+
variant: {
|
|
92
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
93
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
94
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
95
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
96
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
97
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
98
|
+
},
|
|
99
|
+
size: {
|
|
100
|
+
default: 'h-10 px-4 py-2',
|
|
101
|
+
sm: 'h-9 rounded-md px-3',
|
|
102
|
+
lg: 'h-11 rounded-md px-8',
|
|
103
|
+
icon: 'h-10 w-10',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
defaultVariants: { variant: 'default', size: 'default' },
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
interface ButtonProps
|
|
111
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
112
|
+
VariantProps<typeof buttonVariants> {
|
|
113
|
+
asChild?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
|
|
117
|
+
const Comp = asChild ? Slot : 'button';
|
|
118
|
+
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `asChild` Pattern
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
// asChild renders the child element instead of the default element
|
|
126
|
+
// Use for link buttons, custom triggers, etc.
|
|
127
|
+
<Button asChild>
|
|
128
|
+
<Link href="/dashboard">Go to Dashboard</Link>
|
|
129
|
+
</Button>
|
|
130
|
+
|
|
131
|
+
<DialogTrigger asChild>
|
|
132
|
+
<Button variant="outline">Open Dialog</Button>
|
|
133
|
+
</DialogTrigger>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Top UX Patterns with Code
|
|
139
|
+
|
|
140
|
+
### 1. Form with Validation (react-hook-form + zod)
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useForm } from 'react-hook-form';
|
|
144
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
145
|
+
import { z } from 'zod';
|
|
146
|
+
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
|
147
|
+
import { Input } from '@/components/ui/input';
|
|
148
|
+
import { Button } from '@/components/ui/button';
|
|
149
|
+
|
|
150
|
+
const formSchema = z.object({
|
|
151
|
+
email: z.string().email('Invalid email address'),
|
|
152
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
153
|
+
role: z.enum(['admin', 'user', 'viewer']),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
type FormValues = z.infer<typeof formSchema>;
|
|
157
|
+
|
|
158
|
+
export function UserForm({ onSubmit }: { onSubmit: (data: FormValues) => Promise<void> }) {
|
|
159
|
+
const form = useForm<FormValues>({
|
|
160
|
+
resolver: zodResolver(formSchema),
|
|
161
|
+
defaultValues: { email: '', name: '', role: 'user' },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Form {...form}>
|
|
166
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
167
|
+
<FormField
|
|
168
|
+
control={form.control}
|
|
169
|
+
name="email"
|
|
170
|
+
render={({ field }) => (
|
|
171
|
+
<FormItem>
|
|
172
|
+
<FormLabel>Email</FormLabel>
|
|
173
|
+
<FormControl>
|
|
174
|
+
<Input type="email" placeholder="you@example.com" {...field} />
|
|
175
|
+
</FormControl>
|
|
176
|
+
<FormDescription>We'll never share your email.</FormDescription>
|
|
177
|
+
<FormMessage /> {/* Auto-shows zod error messages */}
|
|
178
|
+
</FormItem>
|
|
179
|
+
)}
|
|
180
|
+
/>
|
|
181
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
182
|
+
{form.formState.isSubmitting ? 'Saving...' : 'Save'}
|
|
183
|
+
</Button>
|
|
184
|
+
</form>
|
|
185
|
+
</Form>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 2. Dialog / Modal
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import {
|
|
194
|
+
Dialog, DialogContent, DialogDescription,
|
|
195
|
+
DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
|
|
196
|
+
} from '@/components/ui/dialog';
|
|
197
|
+
|
|
198
|
+
export function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
|
|
199
|
+
const [open, setOpen] = React.useState(false);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
203
|
+
<DialogTrigger asChild>
|
|
204
|
+
<Button variant="destructive">Delete</Button>
|
|
205
|
+
</DialogTrigger>
|
|
206
|
+
<DialogContent className="sm:max-w-md">
|
|
207
|
+
<DialogHeader>
|
|
208
|
+
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
209
|
+
<DialogDescription>
|
|
210
|
+
This action cannot be undone. This will permanently delete your data.
|
|
211
|
+
</DialogDescription>
|
|
212
|
+
</DialogHeader>
|
|
213
|
+
<DialogFooter>
|
|
214
|
+
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
|
|
215
|
+
<Button variant="destructive" onClick={() => { onConfirm(); setOpen(false); }}>
|
|
216
|
+
Delete
|
|
217
|
+
</Button>
|
|
218
|
+
</DialogFooter>
|
|
219
|
+
</DialogContent>
|
|
220
|
+
</Dialog>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 3. Data Table (TanStack Table)
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import {
|
|
229
|
+
useReactTable, getCoreRowModel, getSortedRowModel,
|
|
230
|
+
getPaginationRowModel, getFilteredRowModel,
|
|
231
|
+
type ColumnDef, type SortingState,
|
|
232
|
+
} from '@tanstack/react-table';
|
|
233
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
234
|
+
|
|
235
|
+
interface User { id: string; name: string; email: string; role: string; }
|
|
236
|
+
|
|
237
|
+
const columns: ColumnDef<User>[] = [
|
|
238
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
239
|
+
{ accessorKey: 'email', header: 'Email' },
|
|
240
|
+
{
|
|
241
|
+
accessorKey: 'role',
|
|
242
|
+
header: 'Role',
|
|
243
|
+
cell: ({ row }) => <Badge variant="outline">{row.getValue('role')}</Badge>,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'actions',
|
|
247
|
+
cell: ({ row }) => <UserActions user={row.original} />,
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
export function UsersTable({ data }: { data: User[] }) {
|
|
252
|
+
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
253
|
+
const [globalFilter, setGlobalFilter] = React.useState('');
|
|
254
|
+
|
|
255
|
+
const table = useReactTable({
|
|
256
|
+
data,
|
|
257
|
+
columns,
|
|
258
|
+
state: { sorting, globalFilter },
|
|
259
|
+
onSortingChange: setSorting,
|
|
260
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
261
|
+
getCoreRowModel: getCoreRowModel(),
|
|
262
|
+
getSortedRowModel: getSortedRowModel(),
|
|
263
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
264
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="space-y-4">
|
|
269
|
+
<Input placeholder="Search..." value={globalFilter} onChange={e => setGlobalFilter(e.target.value)} />
|
|
270
|
+
<Table>
|
|
271
|
+
<TableHeader>
|
|
272
|
+
{table.getHeaderGroups().map(group => (
|
|
273
|
+
<TableRow key={group.id}>
|
|
274
|
+
{group.headers.map(header => (
|
|
275
|
+
<TableHead key={header.id}
|
|
276
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
277
|
+
className="cursor-pointer">
|
|
278
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
279
|
+
</TableHead>
|
|
280
|
+
))}
|
|
281
|
+
</TableRow>
|
|
282
|
+
))}
|
|
283
|
+
</TableHeader>
|
|
284
|
+
<TableBody>
|
|
285
|
+
{table.getRowModel().rows.map(row => (
|
|
286
|
+
<TableRow key={row.id}>
|
|
287
|
+
{row.getVisibleCells().map(cell => (
|
|
288
|
+
<TableCell key={cell.id}>
|
|
289
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
290
|
+
</TableCell>
|
|
291
|
+
))}
|
|
292
|
+
</TableRow>
|
|
293
|
+
))}
|
|
294
|
+
</TableBody>
|
|
295
|
+
</Table>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 4. Command Palette
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import {
|
|
305
|
+
CommandDialog, CommandEmpty, CommandGroup,
|
|
306
|
+
CommandInput, CommandItem, CommandList,
|
|
307
|
+
} from '@/components/ui/command';
|
|
308
|
+
|
|
309
|
+
export function CommandPalette() {
|
|
310
|
+
const [open, setOpen] = React.useState(false);
|
|
311
|
+
|
|
312
|
+
React.useEffect(() => {
|
|
313
|
+
const down = (e: KeyboardEvent) => {
|
|
314
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
setOpen(prev => !prev);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
document.addEventListener('keydown', down);
|
|
320
|
+
return () => document.removeEventListener('keydown', down);
|
|
321
|
+
}, []);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
325
|
+
<CommandInput placeholder="Type a command or search..." />
|
|
326
|
+
<CommandList>
|
|
327
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
328
|
+
<CommandGroup heading="Navigation">
|
|
329
|
+
<CommandItem onSelect={() => { router.push('/dashboard'); setOpen(false); }}>
|
|
330
|
+
Dashboard
|
|
331
|
+
</CommandItem>
|
|
332
|
+
<CommandItem onSelect={() => { router.push('/settings'); setOpen(false); }}>
|
|
333
|
+
Settings
|
|
334
|
+
</CommandItem>
|
|
335
|
+
</CommandGroup>
|
|
336
|
+
</CommandList>
|
|
337
|
+
</CommandDialog>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### 5. Toast Notifications with Sonner
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
// In layout — add once
|
|
346
|
+
import { Toaster } from '@/components/ui/sonner';
|
|
347
|
+
<Toaster position="bottom-right" richColors />
|
|
348
|
+
|
|
349
|
+
// In any component
|
|
350
|
+
import { toast } from 'sonner';
|
|
351
|
+
|
|
352
|
+
// Usage
|
|
353
|
+
toast.success('Saved successfully');
|
|
354
|
+
toast.error('Something went wrong');
|
|
355
|
+
toast.promise(saveUser(data), {
|
|
356
|
+
loading: 'Saving...',
|
|
357
|
+
success: 'User saved!',
|
|
358
|
+
error: 'Failed to save',
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 6. Dropdown Menu
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import {
|
|
366
|
+
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
|
367
|
+
DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
|
368
|
+
} from '@/components/ui/dropdown-menu';
|
|
369
|
+
|
|
370
|
+
<DropdownMenu>
|
|
371
|
+
<DropdownMenuTrigger asChild>
|
|
372
|
+
<Button variant="ghost" size="icon"><MoreHorizontal /></Button>
|
|
373
|
+
</DropdownMenuTrigger>
|
|
374
|
+
<DropdownMenuContent align="end">
|
|
375
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
376
|
+
<DropdownMenuSeparator />
|
|
377
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(id)}>
|
|
378
|
+
Copy ID
|
|
379
|
+
</DropdownMenuItem>
|
|
380
|
+
<DropdownMenuItem onClick={() => onEdit(item)}>Edit</DropdownMenuItem>
|
|
381
|
+
<DropdownMenuSeparator />
|
|
382
|
+
<DropdownMenuItem className="text-destructive" onClick={() => onDelete(item)}>
|
|
383
|
+
Delete
|
|
384
|
+
</DropdownMenuItem>
|
|
385
|
+
</DropdownMenuContent>
|
|
386
|
+
</DropdownMenu>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## CSS Variables Theming
|
|
392
|
+
|
|
393
|
+
```css
|
|
394
|
+
/* globals.css — generated by shadcn init */
|
|
395
|
+
@layer base {
|
|
396
|
+
:root {
|
|
397
|
+
--background: 0 0% 100%;
|
|
398
|
+
--foreground: 222.2 84% 4.9%;
|
|
399
|
+
--card: 0 0% 100%;
|
|
400
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
401
|
+
--primary: 222.2 47.4% 11.2%; /* Change this for brand color */
|
|
402
|
+
--primary-foreground: 210 40% 98%;
|
|
403
|
+
--secondary: 210 40% 96.1%;
|
|
404
|
+
--muted: 210 40% 96.1%;
|
|
405
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
406
|
+
--border: 214.3 31.8% 91.4%;
|
|
407
|
+
--ring: 222.2 84% 4.9%;
|
|
408
|
+
--radius: 0.5rem; /* Change for border-radius style */
|
|
409
|
+
}
|
|
410
|
+
.dark {
|
|
411
|
+
--background: 222.2 84% 4.9%;
|
|
412
|
+
--foreground: 210 40% 98%;
|
|
413
|
+
--primary: 210 40% 98%;
|
|
414
|
+
/* ... all tokens redefined for dark */
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Brand customization:** change `--primary` HSL values. All buttons, links, focus rings update automatically. No component edits needed.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Best Practices by Category
|
|
424
|
+
|
|
425
|
+
### Component Modification
|
|
426
|
+
- Edit component files directly — they live in `components/ui/`, not in `node_modules`
|
|
427
|
+
- Use `cn()` for all conditional class merging to avoid Tailwind conflicts
|
|
428
|
+
- Extend with `cva` variants rather than adding one-off className strings
|
|
429
|
+
- Keep customizations in the component file, not via external CSS overrides
|
|
430
|
+
|
|
431
|
+
### Form Patterns
|
|
432
|
+
- Always pair shadcn Form with react-hook-form + zod for type-safe validation
|
|
433
|
+
- Use `FormMessage` — it auto-displays the zod error for the field
|
|
434
|
+
- Use `FormDescription` for helper text below inputs
|
|
435
|
+
- `defaultValues` in `useForm` prevents uncontrolled→controlled warnings
|
|
436
|
+
|
|
437
|
+
### Accessibility
|
|
438
|
+
- Radix UI primitives handle focus trapping, keyboard navigation, ARIA automatically
|
|
439
|
+
- Never remove `role` or `aria-*` attributes from generated components
|
|
440
|
+
- Custom triggers should use `asChild` to preserve semantics
|
|
441
|
+
- Test with keyboard only — tab, enter, escape, arrow keys must work
|
|
442
|
+
|
|
443
|
+
### Dark Mode
|
|
444
|
+
- shadcn ships with dark mode via `.dark` class on `<html>`
|
|
445
|
+
- Next.js: use `next-themes` (`npm install next-themes`) for system preference and toggle
|
|
446
|
+
- All CSS variables have dark counterparts — no component changes needed
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Common Anti-Patterns
|
|
451
|
+
|
|
452
|
+
1. **Installing shadcn as an npm package** — there is no `npm install shadcn`. It's copy-paste by design. Components live in your codebase, not node_modules.
|
|
453
|
+
|
|
454
|
+
2. **Not using `cn()` for conditional classes** — `className={`base ${condition ? 'a' : 'b'}`}` causes Tailwind conflicts. Always use `cn()` for merging.
|
|
455
|
+
|
|
456
|
+
3. **Styling Radix components with external CSS** — use Tailwind classes directly in the component file. External CSS specificity battles are painful.
|
|
457
|
+
|
|
458
|
+
4. **Adding components manually without `npx shadcn add`** — the CLI wires up imports, dependencies, and ensures correct file paths. Manual copying often misses peer deps.
|
|
459
|
+
|
|
460
|
+
5. **Using `!important` to override** — edit the component file instead. You own it.
|
|
461
|
+
|
|
462
|
+
6. **Wrapping every primitive in a div** — Radix's `asChild` prop renders the child element directly. Use it instead of wrapper divs.
|
|
463
|
+
|
|
464
|
+
7. **Passing className without `cn()`** — `<Button className="w-full">` works, but `<Button className={cn('w-full', variant === 'danger' && 'bg-red-500')}>` is safer.
|
|
465
|
+
|
|
466
|
+
8. **Not upgrading component files after `npx shadcn@latest add`** — running add again on an existing component regenerates it. Back up local modifications first.
|
|
467
|
+
|
|
468
|
+
9. **Using `<select>` instead of shadcn Select** — native select is unstyled and inaccessible on some platforms. Use the Radix-backed Select component.
|
|
469
|
+
|
|
470
|
+
10. **Skipping `<DialogDescription>`** — Radix Dialog warns (and NVDA/JAWS users miss context) without it. Always include for screen reader support.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Performance Checklist
|
|
475
|
+
|
|
476
|
+
- [ ] Tree-shaking: only imported components are bundled (no barrel `index.ts` needed)
|
|
477
|
+
- [ ] `asChild` to avoid unnecessary DOM wrappers
|
|
478
|
+
- [ ] Lazy-load Dialog, Sheet, Drawer content: `React.lazy(() => import('./HeavyDialog'))`
|
|
479
|
+
- [ ] TanStack Table pagination — render only visible rows, not all data
|
|
480
|
+
- [ ] `sonner` toasts are lazy-rendered, not always in DOM
|
|
481
|
+
- [ ] Radix portals render outside the DOM tree — no overflow:hidden issues
|
|
482
|
+
- [ ] `cmdk` Command palette uses virtual scrolling for large lists
|
|
483
|
+
- [ ] Avoid re-creating `columns` array on every render — define outside component or `useMemo`
|
|
484
|
+
- [ ] Dark mode via CSS variables — zero JavaScript color computation at runtime
|
|
485
|
+
- [ ] `cn()` uses `twMerge` which is ~2KB — import only where needed
|