Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when building UI components. Enforces ShadCN UI patterns, accessibility standards (Radix UI), and TailwindCSS best practices for November 2025.
.claude/skills/aiskillstore-shadcn-ui-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | 30% | 0% |
| case-01 | ✗→✓ | ▲ Improved | -10% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -9% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 20% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 70% | 0% |
bash# Install individual components as needed npx shadcn@latest add button npx shadcn@latest add dialog npx shadcn@latest add form npx shadcn@latest add input npx shadcn@latest add label
Components are copied to src/components/ui/ directory - you own the code.
typescriptimport { Button } from "@/components/ui/button" // ✅ DO: Use semantic variants <Button variant="default">Save</Button> <Button variant="destructive">Delete</Button> <Button variant="outline">Cancel</Button> <Button variant="ghost">Skip</Button> <Button variant="link">Learn More</Button> // ✅ DO: Use size variants <Button size="default">Medium</Button> <Button size="sm">Small</Button> <Button size="lg">Large</Button> <Button size="icon"><Icon /></Button> // ❌ DON'T: Create custom buttons without using Button component <button className="px-4 py-2 bg-blue-500">Bad</button>
typescriptimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" // ✅ DO: Use proper dialog structure (accessibility) <Dialog> <DialogTrigger asChild> <Button>Open Settings</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Settings</DialogTitle> <DialogDescription> Configure your application settings here. </DialogDescription> </DialogHeader> {/* Dialog content */} </DialogContent> </Dialog> // ❌ DON'T: Skip DialogHeader or DialogTitle (breaks screen readers) <DialogContent> <h2>Settings</h2> {/* Wrong - use DialogTitle */} </DialogContent>
typescriptimport { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" // ✅ DO: Define Zod schema first (validation) const formSchema = z.object({ email: z.string().email("Invalid email address"), password: z.string().min(8, "Password must be at least 8 characters"), }) function LoginForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { email: "", password: "", }, }) async function onSubmit(values: z.infer<typeof formSchema>) { // Type-safe validated data console.log(values) } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input placeholder="you@example.com" {...field} /> </FormControl> <FormDescription> We'll never share your email. </FormDescription> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <Input type="password" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit">Sign In</Button> </form> </Form> ) } // ❌ DON'T: Use uncontrolled forms without validation <form> <input name="email" /> {/* No validation */} </form>
typescript// ✅ DO: Use Server Component for static dialogs import { Dialog, DialogContent } from "@/components/ui/dialog" export default function ServerDialog() { // No 'use client' needed return <Dialog>...</Dialog> } // ✅ DO: Use Client Component when state is needed 'use client' import { useState } from 'react' import { Dialog, DialogContent } from "@/components/ui/dialog" export function ClientDialog() { const [open, setOpen] = useState(false) return ( <Dialog open={open} onOpenChange={setOpen}> <DialogContent>...</DialogContent> </Dialog> ) }
typescript// ✅ DO: Use DialogTrigger with asChild for proper focus <DialogTrigger asChild> <Button>Open</Button> </DialogTrigger> // ❌ DON'T: Manually trigger without proper focus handling <Button onClick={() => setOpen(true)}>Open</Button>
typescript// ✅ ShadCN handles this automatically: // - ESC closes dialogs // - Tab navigates focusable elements // - Enter/Space activates buttons // - Arrow keys navigate menus // ❌ DON'T: Override default keyboard behavior without good reason
typescript// ✅ DO: Always include DialogTitle (required for ARIA) <DialogHeader> <DialogTitle>Delete Project</DialogTitle> <DialogDescription> This action cannot be undone. </DialogDescription> </DialogHeader> // ❌ DON'T: Use visually hidden titles incorrectly <DialogTitle className="sr-only">Delete</DialogTitle> // Only hide if there's a clear visual alternative
| Component | Use Case | Key Props | |-----------|----------|-----------| | Button | All clickable actions | variant, size, asChild | | Dialog | Modals, confirmations | open, onOpenChange | | Sheet | Side panels, drawers | side, open, onOpenChange | | Popover | Tooltips, menus | open, onOpenChange | | Form | All forms | form (from useForm) | | Input | Text input | type, placeholder | | Select | Dropdowns | value, onValueChange | | Checkbox | Boolean input | checked, onCheckedChange | | RadioGroup | Single choice | value, onValueChange | | Table | Data tables | table (from TanStack Table) | | Card | Content containers | CardHeader, CardContent, CardFooter | | Toast | Notifications | title, description, variant | | Command | Command palette | onSelect | | Tabs | Tab navigation | value, onValueChange |
typescript// ✅ DO: Use Tailwind utility classes <Button className="w-full mt-4">Submit</Button> // ✅ DO: Use cn() helper for conditional classes import { cn } from "@/lib/utils" <Button className={cn( "w-full", isLoading && "opacity-50 cursor-not-allowed" )}> Submit </Button> // ❌ DON'T: Use inline styles <Button style={{ width: '100%', marginTop: '16px' }}>Submit</Button> // ❌ DON'T: Create custom CSS files for components // styles.css .my-button { width: 100%; }
typescript// ✅ DO: Use Tailwind dark mode classes <div className="bg-white dark:bg-gray-900 text-black dark:text-white"> Content </div> // ✅ ShadCN components have dark mode built-in <Button variant="default"> {/* Automatically styled for dark mode */} </Button>
typescript// BAD <DialogContent> <h2>Settings</h2> <p>Content</p> </DialogContent> // GOOD <DialogContent> <DialogHeader> <DialogTitle>Settings</DialogTitle> </DialogHeader> <p>Content</p> </DialogContent>
typescript// BAD - No validation, poor UX <form> <input name="email" /> <button type="submit">Submit</button> </form> // GOOD - Validation, error messages, accessibility <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <FormField name="email" ... /> </form> </Form>
typescript// BAD <Button className="bg-red-500 hover:bg-red-600">Delete</Button> // GOOD <Button variant="destructive">Delete</Button>
typescript// BAD - Creates unnecessary nested buttons <DialogTrigger> <Button>Open</Button> </DialogTrigger> // Renders: <button><button>Open</button></button> (invalid HTML) // GOOD - Merges props into single button <DialogTrigger asChild> <Button>Open</Button> </DialogTrigger> // Renders: <button>Open</button>
typescriptimport { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Dialog, DialogTrigger, DialogContent } from '@/components/ui/dialog' describe('Dialog', () => { it('should open when trigger is clicked', async () => { const user = userEvent.setup() render( <Dialog> <DialogTrigger asChild> <button>Open</button> </DialogTrigger> <DialogContent> <div>Dialog content</div> </DialogContent> </Dialog> ) // Dialog content should not be visible initially expect(screen.queryByText('Dialog content')).not.toBeInTheDocument() // Click trigger await user.click(screen.getByText('Open')) // Dialog content should now be visible expect(screen.getByText('Dialog content')).toBeInTheDocument() }) it('should close on ESC key', async () => { const user = userEvent.setup() render( <Dialog defaultOpen> <DialogContent>Dialog content</DialogContent> </Dialog> ) expect(screen.getByText('Dialog content')).toBeInTheDocument() await user.keyboard('{Escape}') expect(screen.queryByText('Dialog content')).not.toBeInTheDocument() }) })
ShadCN UI is the industry standard for React component libraries as of November 2025. All new Quetrex applications must use ShadCN UI for consistency, accessibility, and maintainability.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | fail→pass | 34,083 | 8,261 | -76% | 1 | 1 | 0% | 3,415 | 4,448 | +30% | 0 | 0 | — |
case-01 | fail→pass | 23,496 | 14,228 | -39% | 1 | 1 | 0% | 5,520 | 4,972 | -10% | 0 | 0 | — |
case-02 | fail→pass | 28,856 | 20,381 | -29% | 1 | 1 | 0% | 6,380 | 5,795 | -9% | 0 | 0 | — |
case-03 | fail→pass | 17,872 | 8,788 | -51% | 1 | 1 | 0% | 4,088 | 4,892 | +20% | 0 | 0 | — |
case-04 | pass→pass | 19,205 | 8,558 | -55% | 1 | 1 | 0% | 2,189 | 4,754 | +117% | 0 | 0 | — |
case-05 | fail→pass | 20,230 | 10,322 | -49% | 1 | 1 | 0% | 2,912 | 4,964 | +70% | 0 | 0 | — |
case-06 | fail→pass | 13,756 | 12,357 | -10% | 1 | 1 | 0% | 2,828 | 5,642 | +100% | 0 | 0 | — |
case-07 | pass→pass | 21,011 | 14,685 | -30% | 1 | 1 | 0% | 3,596 | 5,038 | +40% | 0 | 0 | — |
case-08 | fail→pass | 36,476 | 17,055 | -53% | 1 | 1 | 0% | 2,614 | 5,273 | +102% | 0 | 0 | — |
case-09 | pass→pass | 17,014 | 11,389 | -33% | 1 | 1 | 0% | 2,383 | 5,585 | +134% | 0 | 0 | — |
case-10 | pass→pass | 24,267 | 16,050 | -34% | 1 | 1 | 0% | 3,945 | 6,199 | +57% | 0 | 0 | — |
case-11 | pass→pass | 11,121 | 14,085 | +27% | 1 | 1 | 0% | 2,056 | 4,591 | +123% | 0 | 0 | — |
case-12 | fail→pass | 25,931 | 18,772 | -28% | 1 | 1 | 0% | 5,102 | 6,014 | +18% | 0 | 0 | — |
case-13 | fail→pass | 25,943 | 16,495 | -36% | 1 | 1 | 0% | 4,658 | 5,520 | +19% | 0 | 0 | — |
case-14 | fail→pass | 23,784 | 18,885 | -21% | 1 | 1 | 0% | 3,960 | 5,620 | +42% | 0 | 0 | — |
case-15 | fail→pass | 17,426 | 17,033 | -2% | 1 | 1 | 0% | 2,615 | 5,228 | +100% | 0 | 0 | — |
case-17 | fail→pass | 14,594 | 17,590 | +21% | 1 | 1 | 0% | 1,801 | 5,283 | +193% | 0 | 0 | — |
case-18 | fail→pass | 36,512 | 24,679 | -32% | 1 | 1 | 0% | 7,605 | 4,634 | -39% | 0 | 0 | — |
case-19 | fail→pass | 40,939 | 15,444 | -62% | 1 | 1 | 0% | 3,258 | 5,167 | +59% | 0 | 0 | — |
case-20 | pass→pass | 28,392 | 11,045 | -61% | 1 | 1 | 0% | 1,834 | 4,247 | +132% | 0 | 0 | — |
case-21 | pass→pass | 19,902 | 28,790 | +45% | 1 | 1 | 0% | 2,945 | 6,553 | +123% | 0 | 0 | — |
case-22 | pass→pass | 9,093 | 29,475 | +224% | 1 | 1 | 0% | 1,708 | 4,764 | +179% | 0 | 0 | — |
case-23 | fail→pass | 32,221 | 33,009 | +2% | 1 | 1 | 0% | 3,386 | 5,849 | +73% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +65 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.