--- name: shadcn-ui-patterns description: Use when building UI components. Enforces ShadCN UI patterns, accessibility standards (Radix UI), and TailwindCSS best practices for November 2025. allowed-tools: Read, Grep, Glob --- # ShadCN UI Patterns - November 2025 Standards ## When to Use - Building new UI components - Refactoring existing components to use ShadCN - Implementing forms with validation - Creating modals, dialogs, and overlays - Ensuring accessibility compliance ## Why ShadCN UI? - **Copy-paste, not npm** - Full ownership of component code - **Radix UI primitives** - Accessibility built-in (WCAG 2.1 AA compliant) - **TailwindCSS-first** - Full customization, no CSS-in-JS - **TypeScript-native** - Type-safe props and variants - **Server Component compatible** - Works with Next.js 15 App Router ## Core Principles ### 1. Component Installation Pattern ```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. ### 2. Component Usage Patterns #### Button Component ```typescript import { Button } from "@/components/ui/button" // ✅ DO: Use semantic variants // ✅ DO: Use size variants // ❌ DON'T: Create custom buttons without using Button component ``` #### Dialog/Modal Component ```typescript import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" // ✅ DO: Use proper dialog structure (accessibility) Settings Configure your application settings here. {/* Dialog content */} // ❌ DON'T: Skip DialogHeader or DialogTitle (breaks screen readers)

Settings

{/* Wrong - use DialogTitle */}
``` #### Form Component (with React Hook Form + Zod) ```typescript import { 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>({ resolver: zodResolver(formSchema), defaultValues: { email: "", password: "", }, }) async function onSubmit(values: z.infer) { // Type-safe validated data console.log(values) } return (
( Email We'll never share your email. )} /> ( Password )} /> ) } // ❌ DON'T: Use uncontrolled forms without validation
{/* No validation */}
``` ### 3. Server vs Client Components ```typescript // ✅ DO: Use Server Component for static dialogs import { Dialog, DialogContent } from "@/components/ui/dialog" export default function ServerDialog() { // No 'use client' needed return ... } // ✅ 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 ( ... ) } ``` ### 4. Accessibility Requirements #### Focus Management ```typescript // ✅ DO: Use DialogTrigger with asChild for proper focus // ❌ DON'T: Manually trigger without proper focus handling ``` #### Keyboard Navigation ```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 ``` #### Screen Reader Support ```typescript // ✅ DO: Always include DialogTitle (required for ARIA) Delete Project This action cannot be undone. // ❌ DON'T: Use visually hidden titles incorrectly Delete // Only hide if there's a clear visual alternative ``` ### 5. Common Components to Use | 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` | ### 6. TailwindCSS Best Practices ```typescript // ✅ DO: Use Tailwind utility classes // ✅ DO: Use cn() helper for conditional classes import { cn } from "@/lib/utils" // ❌ DON'T: Use inline styles // ❌ DON'T: Create custom CSS files for components // styles.css .my-button { width: 100%; } ``` ### 7. Dark Mode Support ```typescript // ✅ DO: Use Tailwind dark mode classes
Content
// ✅ ShadCN components have dark mode built-in ``` ## Common Mistakes to Catch ### ❌ Missing DialogTitle (Accessibility Violation) ```typescript // BAD

Settings

Content

// GOOD Settings

Content

``` ### ❌ Not Using Form Component for Forms ```typescript // BAD - No validation, poor UX
// GOOD - Validation, error messages, accessibility
``` ### ❌ Hardcoding Colors Instead of Using Variants ```typescript // BAD // GOOD ``` ### ❌ Not Using asChild for Triggers ```typescript // BAD - Creates unnecessary nested buttons // Renders: (invalid HTML) // GOOD - Merges props into single button // Renders: ``` ## Testing ShadCN Components ```typescript import { 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 content
) // 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 content ) expect(screen.getByText('Dialog content')).toBeInTheDocument() await user.keyboard('{Escape}') expect(screen.queryByText('Dialog content')).not.toBeInTheDocument() }) }) ``` ## Resources - **Official Docs**: https://ui.shadcn.com - **Radix UI**: https://www.radix-ui.com - **Examples**: https://ui.shadcn.com/examples - **Themes**: https://ui.shadcn.com/themes ## November 2025 Note 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.