Ensure accessibility in UI components including semantic HTML, ARIA attributes, keyboard navigation, and WCAG 2.2 AA compliance.
日本語の概要は準備中です。原文の説明を表示しています。
Deep expertise on shadcn/ui component library including installation, customization, theming, and accessibility patterns. Use when building UI with shadcn/ui components, integrating Radix UI primitives, configuring Tailwind CSS v4 themes, or implementing dark mode. Covers Next.js App Router, Pages Router, and Vite setups.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Comprehensive guide to building UIs with shadcn/ui -- the copy-paste component library built on Radix UI primitives and Tailwind CSS. Components are not installed as a dependency; they are copied into your project for full ownership and customization.
Use this skill when:
shadcn/ui is a collection of reusable components that you copy into your project. Key differences from traditional libraries:
npx shadcn@latest add button copies component codeYour Project
components/
ui/ <- shadcn/ui components live here
button.tsx
dialog.tsx
input.tsx
...
lib/
utils.ts <- cn() utility (clsx + tailwind-merge)
# Initialize shadcn/ui in existing Next.js project
npx shadcn@latest init
# This creates:
# - components.json (configuration)
# - lib/utils.ts (cn utility)
# - Tailwind CSS variable theme in globals.css
components.json Configuration:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
# Initialize
npx shadcn@latest init
# Vite requires path aliases in vite.config.ts:
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Same as App Router but set rsc: false in components.json since Pages Router does not support React Server Components.
# Add a single component
npx shadcn@latest add button
# Add multiple components
npx shadcn@latest add button card input label
# Add all components
npx shadcn@latest add --all
# View available components
npx shadcn@latest add --list
Every shadcn/ui component uses cn() for conditional class merging:
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
cn() combines clsx (conditional classes) with tailwind-merge (resolves Tailwind conflicts):
<Button
className={cn(
'bg-primary text-white',
isDisabled && 'opacity-50 cursor-not-allowed',
size === 'lg' && 'px-8 py-4 text-lg'
)}
>
Submit
</Button>
shadcn/ui uses CSS custom properties for theming, enabling runtime theme switching:
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
Use next-themes for Next.js dark mode:
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Toggle component:
'use client';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';
export function ThemeToggle() {
const { setTheme, theme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: '', password: '' },
});
function onSubmit(values: z.infer<typeof formSchema>) {
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="name@example.com" {...field} />
</FormControl>
<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>
);
}
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-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.map(row => (
<TableRow key={row.id}>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
Use Dialog on desktop and Drawer on mobile:
'use client';
import { useMediaQuery } from '@/hooks/use-media-query';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import { Button } from '@/components/ui/button';
export function ResponsiveModal({ children }: { children: React.ReactNode }) {
const isDesktop = useMediaQuery('(min-width: 768px)');
if (isDesktop) {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>{children}</DialogContent>
</Dialog>
);
}
return (
<Drawer>
<DrawerTrigger asChild>
<Button>Open</Button>
</DrawerTrigger>
<DrawerContent>{children}</DrawerContent>
</Drawer>
);
}
shadcn/ui components are built on Radix UI, which provides:
| Component | Keyboard | ARIA | Focus Trap |
|---|---|---|---|
| Button | Enter/Space to activate | role="button" | No |
| Dialog | Escape to close | role="dialog", aria-modal | Yes |
| Dropdown Menu | Arrow keys to navigate | role="menu", role="menuitem" | Yes |
| Select | Arrow keys, type-ahead | role="listbox", role="option" | Yes |
| Tabs | Arrow keys between tabs | role="tablist", role="tab" | No |
| Toast | Auto-announce | role="status", aria-live | No |
| Tooltip | Focus/hover to show | role="tooltip" | No |
// Always provide labels for interactive elements
<Button aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
// Use sr-only for visual-only content
<span className="sr-only">Loading...</span>
// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
cn() utility -- it prevents Tailwind class conflictssuppressHydrationWarning on <html> when using next-themescn() utility for conditional class names to prevent Tailwind class conflicts| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Installing as a package | Component source is locked; no customization possible | Use npx shadcn@latest add to copy components into your project |
| Hardcoding color values | Theme switching breaks; dark mode fails | Use CSS variable tokens (bg-background, text-foreground, etc.) |
Skipping cn() utility | Tailwind class conflicts produce unpredictable styles | Always merge classes with cn() from @/lib/utils |
| Direct Radix UI primitive use | Missing shadcn styling and accessibility wiring | Use shadcn components that wrap Radix primitives with correct classes |
Missing suppressHydrationWarning | Hydration mismatch errors with next-themes dark mode | Add suppressHydrationWarning to <html> when using next-themes |
Before starting:
Read .claude/context/memory/learnings.md
After completing:
.claude/context/memory/learnings.md.claude/context/memory/issues.md.claude/context/memory/decisions.mdASSUME INTERRUPTION: If it's not in memory, it didn't happen.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Ensure accessibility in UI components including semantic HTML, ARIA attributes, keyboard navigation, and WCAG 2.2 AA compliance.
日本語の概要は準備中です。原文の説明を表示しています。
Use when you want to improve response quality through meta-cognitive reasoning. Applies 15+ reasoning methods to reconsider and refine initial outputs.
日本語の概要は準備中です。原文の説明を表示しています。
N-round opposing-stance debates for trade-off analysis. Assigns pro/con roles to agents, runs structured debate rounds with quality scoring, and produces a moderator synthesis with confidence-rated recommendation. Generalizable to architecture, technology, security, and design decisions.
日本語の概要は準備中です。原文の説明を表示しています。
Force adversarial code review stance that eliminates confirmation bias — reviewer must find issues or re-analyze
日本語の概要は準備中です。原文の説明を表示しています。
Creates specialized AI agents on-demand when no existing agent matches a request. Use when the Router cannot find a suitable agent for a task. Enables self-evolution by generating persistent agents.
日本語の概要は準備中です。原文の説明を表示しています。
LLM-as-judge evaluation framework with 5-dimension rubric (accuracy, groundedness, coherence, completeness, helpfulness) for scoring AI-generated content quality with weighted composite scores and evidence citations
日本語の概要は準備中です。原文の説明を表示しています。