shadcn-ui
shadcn/ui component patterns for Next.js 16 applications. This skill should be used when adding UI components, customizing component styles, composing primitives, or integrating forms with react-hook-form. Covers installation, customization, composition patterns, and Atlas-specific conventions using Tailwind CSS v4.
Install
mkdir -p .claude/skills/shadcn-ui && curl -L -o skill.zip "https://mcp.directory/api/skills/download/278" && unzip -o skill.zip -d .claude/skills/shadcn-ui && rm skill.zipInstalls to .claude/skills/shadcn-ui
About this skill
shadcn/ui Component Usage
Purpose
Provide comprehensive patterns for implementing shadcn/ui components in Next.js 16 applications with Atlas-specific conventions. Focus on composition over props, accessibility-first design, and type-safe integration with tRPC and react-hook-form.
When To Use This Skill
Component Implementation:
- Adding new shadcn/ui components to the project
- Customizing component variants and styles
- Building composite components from primitives (Dialog, Form, Table)
- Implementing responsive mobile/desktop patterns
Form Integration:
- Integrating react-hook-form with shadcn Form components
- Validating forms with Zod schemas
- Connecting forms to tRPC mutations
- Handling form state and errors
Data Display:
- Creating data tables with sorting and filtering
- Building card layouts and list views
- Implementing skeleton loading states
Interactive Patterns:
- Building modal dialogs and drawers (sheets)
- Implementing toast notifications
- Creating dropdown menus and popovers
- Adding tooltips and hover states
Accessibility:
- Ensuring keyboard navigation works correctly
- Adding proper ARIA labels (especially icon buttons)
- Implementing focus management
- Meeting WCAG 2.1 AAA standards (44px minimum touch targets)
Core Principles
1. Copy-Not-Import Philosophy
shadcn/ui components are copied into the project, not imported as dependencies. Customize directly in src/components/ui/.
// ✅ Customize directly
// src/components/ui/button.tsx
const buttonVariants = cva("...", {
variants: {
variant: {
default: "bg-primary text-primary-foreground",
// Add your custom variant
atlas: "bg-blue-600 text-white hover:bg-blue-700",
},
},
});
2. Composition Over Props
Build complex components by composing primitives rather than adding props:
// ❌ Avoid: Too many props
<Dialog
title="Delete Item"
description="Are you sure?"
showCloseButton={true}
size="lg"
/>
// ✅ Prefer: Composition
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Item</DialogTitle>
<DialogDescription>Are you sure?</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
3. className Customization
Extend styles via className prop using Tailwind utilities:
<Button variant="default" className="w-full sm:w-auto shadow-lg">
Submit
</Button>
4. Accessibility First
All components are accessible by default (built on Radix UI):
- Keyboard navigation
- Screen reader support
- ARIA attributes
- Focus management
Icon buttons require labels for accessibility:
// ❌ Inaccessible
<Button size="icon">
<TrashIcon />
</Button>
// ✅ Accessible
<Button size="icon" aria-label="Delete item">
<TrashIcon />
</Button>
Quick Reference
Common Components
| Component | Use Case | Key Features |
|---|---|---|
Button | Actions, triggers | Variants, sizes, loading state |
Input | Text entry | Types, validation states |
Form | Form validation | react-hook-form integration |
Dialog | Modals | Portal, overlay, animations |
Sheet | Side panels | Mobile-friendly drawers |
Table | Data display | Semantic HTML, responsive |
Select | Dropdowns | Searchable, keyboard nav |
Checkbox | Boolean input | Indeterminate state |
Label | Form labels | Auto-linked to inputs |
Textarea | Multi-line input | Auto-resize support |
Tabs | Navigation | Keyboard accessible |
Card | Content containers | Header, content, footer |
Badge | Status labels | Variants for states |
Skeleton | Loading states | Placeholder UI |
Separator | Visual dividers | Horizontal/vertical |
Dropdown Menu | Actions menu | Nested menus, shortcuts |
Alert | Notifications | Info, warning, error |
Progress | Loading indicators | Determinate/indeterminate |
Tooltip | Hover hints | Delay, positioning |
Button Variants & Sizes
<Button variant="default">Primary Action</Button>
<Button variant="secondary">Secondary Action</Button>
<Button variant="outline">Outlined</Button>
<Button variant="ghost">Subtle</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Link Style</Button>
<Button size="sm">Small</Button> {/* 32px mobile, 40px desktop */}
<Button size="default">Default</Button> {/* 44px mobile, 36px desktop */}
<Button size="lg">Large</Button> {/* 48px mobile, 40px desktop */}
<Button size="icon" aria-label="Add"> {/* 44x44px - WCAG 2.1 AAA */}
<PlusIcon />
</Button>
Loading States
<Button loading={isPending} loadingText="Saving...">
Save Changes
</Button>
// Or manually:
<Button disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
Form Integration
Basic Form Pattern
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { 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";
const schema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
type FormValues = z.infer<typeof schema>;
export function MyFormClient() {
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { email: "", name: "" },
});
const onSubmit = form.handleSubmit(async (data) => {
// Handle submission
});
return (
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormDescription>We'll never share your email.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" loading={form.formState.isSubmitting}>
Submit
</Button>
</form>
</Form>
);
}
Form with tRPC Mutation
"use client";
import { api } from "@/lib/api/react";
import { toast } from "sonner";
export function CreateStackFormClient() {
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
/* ... */
},
});
const utils = api.useUtils();
const createMutation = api.stacks.create.useMutation({
onSuccess: () => {
toast.success("Stack created successfully");
utils.stacks.list.invalidate(); // Refetch list
form.reset();
},
onError: (error) => {
toast.error(error.message);
},
});
const onSubmit = form.handleSubmit((data) => {
createMutation.mutate(data);
});
return (
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-4">
{/* Form fields */}
<Button
type="submit"
loading={createMutation.isPending}
disabled={createMutation.isPending}
>
Create Stack
</Button>
</form>
</Form>
);
}
Common Form Field Patterns
Number Input:
<FormField
control={form.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel>Quantity</FormLabel>
<FormControl>
<Input
type="number"
inputMode="numeric"
placeholder="100"
{...field}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Date Input:
<FormField
control={form.control}
name="scheduledFor"
render={({ field: { value, onChange, ...field } }) => (
<FormItem>
<FormLabel>Scheduled Date</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={value instanceof Date ? value.toISOString().split("T")[0] : ""}
onChange={(e) => {
const dateStr = e.target.value;
if (dateStr) onChange(new Date(dateStr));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Select/Dropdown:
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>;
Textarea:
import { Textarea } from "@/components/ui/textarea";
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea placeholder="Optional notes..." maxLength={1000} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>;
Checkbox:
import { Checkbox } from "@/components/ui/checkbox";
<FormField
control={form.control}
name="acceptTerms"
render={({ field }) => (
<FormItem className="flex flex-row items-start gap-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Accept terms and conditions</FormLabel>
<FormDescription>You agree to our Terms of Service.</FormDescription>
</div>
</FormItem>
)}
/>;
Dialog Patterns
Basic Dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>;
Controlled Dialog with Form
"use client";
import { useState } from "react";
export function CreateDialogClient() {
const [open, setOpen] = useState(false);
const form = useForm({
/* ... */
});
const createMutation = api.stacks.create.useMutation({
onSuccess: () => {
toast.success("Created successfully");
form.reset();
setOpen(false); // Close dialog
},
});
const onSubmit = form.handleSubmit((data) => {
createMutation.mutate(data);
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Create New</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Stack</DialogTitle>
<DialogDescription>Add a new stack to the system.</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-4">
{/* Form fields */}
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={createMutation.isPending}
>
Cancel
</Button>
<Button type="submit" loading={createMutation.isPending}>
Create
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
Responsive Dialog/Sheet
Atlas has a custom ResponsiveDialogSheet that shows Dialog on desktop, Sheet on mobile:
import { ResponsiveDialogSheet } from "@/components/features/common";
<ResponsiveDialogSheet
open={open}
onOpenChange={setOpen}
title="Create Stack"
description="Add a new stack to the system"
trigger={<Button>Open</Button>}
>
{/* Content - works on both desktop and mobile */}
<Form {...form}>
<form onSubmit={onSubmit}>{/* Fields */}</form>
</Form>
</ResponsiveDialogSheet>;
Data Table Patterns
Basic Table
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell>{invoice.invoice}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>;
Loading Skeleton
import { Skeleton } from "@/components/ui/skeleton";
if (isLoading) {
return (
<div className="space-y-3">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
);
}
Toast Notifications
Atlas uses Sonner for toast notifications:
import { toast } from "sonner";
// Success
toast.success("Stack created successfully");
// Error
toast.error("Failed to create stack");
// Info
toast.info("Processing your request...");
// Warning
toast.warning("This action is irreversible");
// With action
toast.success("Stack deleted", {
action: {
label: "Undo",
onClick: () => {
/* Undo logic */
},
},
});
// Promise toast (auto-updates based on promise state)
toast.promise(createMutation.mutateAsync(data), {
loading: "Creating stack...",
success: "Stack created successfully",
error: "Failed to create stack",
});
Styling Patterns
Responsive Design
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Mobile: 1 column, Tablet: 2 columns, Desktop: 3 columns */}
</div>
<Button className="w-full sm:w-auto">
{/* Full width on mobile, auto on desktop */}
</Button>
Conditional Styles with cn()
import { cn } from "@/lib/utils";
<Button
className={cn(
"base-classes",
isActive && "bg-blue-600",
isDisabled && "opacity-50 cursor-not-allowed",
variant === "large" && "text-lg px-6",
)}
>
Click me
</Button>;
Dark Mode Support
All shadcn components support dark mode automatically via CSS variables:
<div className="bg-background text-foreground">
{/* Automatically adapts to light/dark mode */}
</div>
Common Mistakes
❌ Missing aria-label on icon buttons
// Bad
<Button size="icon"><TrashIcon /></Button>
// Good
<Button size="icon" aria-label="Delete item"><TrashIcon /></Button>
❌ Not using FormControl
// Bad - missing ARIA attributes
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Input {...field} /> {/* Missing FormControl */}
<FormMessage />
</FormItem>
)}
/>
// Good - proper ARIA linkage
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
❌ Forgetting DialogTrigger asChild
// Bad - creates nested button
<DialogTrigger><Button>Open</Button></DialogTrigger>
// Good - merges props into Button
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
Installation
# Install shadcn CLI
pnpm dlx shadcn@latest init
# Add components
pnpm dlx shadcn@latest add button input form dialog table select checkbox textarea label
Resources
- Official docs: https://ui.shadcn.com
- Examples: See
references/examples.md - Recipes: See
references/recipes.md - Patterns: See
references/patterns.md - Atlas patterns:
@/framework/patterns/shadcn.md
Summary
Key practices:
- Copy, don't import - customize in
src/components/ui/ - Compose, don't prop - build from primitives
- className over props - extend with Tailwind
- Accessibility first - labels on icon buttons
- Forms with react-hook-form - use Form components
- Mobile-first - 44px minimum touch targets
- Type-safe - derive types from tRPC RouterOutputs
You might also like
flutter-development
aj-geddes
Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
drawio-diagrams-enhanced
jgtolentino
Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.
godot
bfollington
This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.
nano-banana-pro
garg-aayush
Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.
ui-ux-pro-max
nextlevelbuilder
"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
rust-coding-skill
UtakataKyosui
Guides Claude in writing idiomatic, efficient, well-structured Rust code using proper data modeling, traits, impl organization, macros, and build-speed best practices.
Stay ahead of the MCP ecosystem
Get weekly updates on new skills and servers.