
React Shadcn
- 54 installs
- 22 repo stars
- Updated August 3, 2026
- fusengine/agents
Build React UI components, forms, dialogs, tables and toasts with shadcn/ui on Radix and Tailwind, plus TanStack Form.
About
Provides shadcn/ui guidance for React 19 with Tailwind 4 and TanStack Form, covering accessible components like dialogs, tables and toasts. A developer uses it when building UI components and forms with shadcn/ui.
- Covers a broad shadcn/ui component reference set
- Uses Radix primitives with Tailwind styling and TanStack Form
React Shadcn by the numbers
- 54 all-time installs (skills.sh)
- Ranked #1,272 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fusengine/agents --skill react-shadcnAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 3, 2026 |
| Repository | fusengine/agents ↗ |
What it does
Build React UI components, forms, dialogs, tables and toasts with shadcn/ui on Radix and Tailwind, plus TanStack Form.
Files
shadcn/ui for React
Beautiful, accessible components built on Radix UI with Tailwind CSS styling.
Agent Workflow (MANDATORY)
Before ANY implementation, use TeamCreate to spawn 3 agents:
1. fuse-ai-pilot:explore-codebase - Analyze existing components and patterns 2. fuse-ai-pilot:research-expert - Verify latest shadcn/ui docs via Context7/Exa 3. mcp__shadcn__* - Search registry for component availability
After implementation, run fuse-ai-pilot:sniper for validation.
---
Overview
When to Use
- Building UI components for React applications (Vite, CRA)
- Need accessible, customizable form components (inputs, selects, checkboxes)
- Implementing dialogs, sheets, drawers, or overlay patterns
- Creating data tables with sorting, filtering, and pagination
- Building navigation menus, sidebars, or command palettes
- Need toast notifications or alert feedback components
Why shadcn/ui
| Feature | Benefit |
|---|---|
| Copy/paste model | Components copied to your project, full ownership |
| Radix UI foundation | Accessibility built-in, unstyled primitives |
| Tailwind CSS styling | Utility-first, easy customization |
| TanStack Form ready | Modern form library with Field pattern |
| Lucide icons | Consistent, customizable icon set |
---
Critical Rules
1. NEVER create components manually - Always install with bunx --bun shadcn@latest add 2. TanStack Form only - NOT React Hook Form for all form implementations 3. Radix UI primitives - Components built on Radix (NOT Base UI) 4. Lucide icons - Default icon library, NOT Remix icons or others 5. Field pattern - Use Field, FieldLabel, FieldError for form fields 6. SOLID paths - Components at @/modules/cores/shadcn/components/ui/
---
Architecture
Component Foundation
- Radix UI - Headless, accessible primitives (Dialog, Select, Popover, Tabs)
- Tailwind CSS v4 - Styling via utility classes, CSS-first config
- class-variance-authority - Variant management for component styles
- clsx + tailwind-merge - Conditional class composition via
cn()utility
Project Structure
Components installed to @/modules/cores/shadcn/components/ui/ following SOLID architecture. Utils at @/modules/cores/lib/utils.ts with cn() helper function.
---
MCP Server Integration
Create .mcp.json at project root for Claude Code integration with shadcn registry.
Available MCP Tools
mcp__shadcn__search_items_in_registries- Search available componentsmcp__shadcn__view_items_in_registries- View component source codemcp__shadcn__get_item_examples_from_registries- Get usage examplesmcp__shadcn__get_add_command_for_items- Get installation commands
See installation.md for complete MCP setup.
---
Component Categories
| Category | Components | Primary Reference |
|---|---|---|
| Setup | Init, configuration, theming, icons | installation.md |
| Forms | Button, Input, Field, Select, Checkbox, Switch, Slider | field-patterns.md |
| Overlay | Dialog, Sheet, Drawer, Popover, Tooltip, HoverCard | dialog.md |
| Feedback | Alert, Toast (Sonner), Progress, Skeleton, Spinner | toast.md |
| Data Display | Table, Badge, Avatar, Calendar, Chart, Carousel | table.md |
| Navigation | Breadcrumb, DropdownMenu, Command, Sidebar, Tabs | sidebar.md |
| Layout | Card, Accordion, Separator, ScrollArea, Resizable | card.md |
---
Best Practices
1. Field components - Use new Field pattern for consistent form field structure 2. Client Components - React apps are client-side by default 3. Sonner for toasts - Modern toast notifications over legacy toast 4. MCP tools first - Use mcp__shadcn__* to explore before implementing 5. Theming via CSS variables - Customize colors in index.css :root 6. Accessibility - Rely on Radix UI keyboard navigation and ARIA
---
Reference Guide
| Need | Reference |
|---|---|
| Initial setup | installation.md, configuration.md |
| Form patterns | field-patterns.md, form-examples.md |
| Theme customization | theming.md |
| Data tables | table.md |
| Modal dialogs | dialog.md, alert-dialog.md |
| Navigation | sidebar.md, navigation-menu.md |
Accordion Component
Accessible accordion component for creating collapsible content sections using Radix UI primitives.
Installation
bunx --bun shadcn-ui@latest add accordionBasic Accordion
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
export function BasicAccordion() {
return (
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles you can customize.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It is animated by default, but you can disable it.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Single vs Multiple Mode
Single Mode (one item open at a time)
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
export function SingleModeAccordion() {
return (
<Accordion type="single" collapsible>
<AccordionItem value="section-1">
<AccordionTrigger>Section 1</AccordionTrigger>
<AccordionContent>Content for section 1</AccordionContent>
</AccordionItem>
<AccordionItem value="section-2">
<AccordionTrigger>Section 2</AccordionTrigger>
<AccordionContent>Content for section 2</AccordionContent>
</AccordionItem>
</Accordion>
)
}Multiple Mode (multiple items open simultaneously)
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
export function MultipleModeAccordion() {
return (
<Accordion type="multiple">
<AccordionItem value="item-1">
<AccordionTrigger>Item 1</AccordionTrigger>
<AccordionContent>Content for item 1</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Item 2</AccordionTrigger>
<AccordionContent>Content for item 2</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Item 3</AccordionTrigger>
<AccordionContent>Content for item 3</AccordionContent>
</AccordionItem>
</Accordion>
)
}FAQ Pattern
Common pattern for frequently asked questions:
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
const faqs = [
{
id: "faq-1",
question: "How do I get started?",
answer:
"To get started, follow the installation steps in our documentation.",
},
{
id: "faq-2",
question: "What is your pricing model?",
answer: "We offer flexible pricing plans based on your usage needs.",
},
{
id: "faq-3",
question: "Do you provide customer support?",
answer: "Yes, we offer 24/7 customer support via email and chat.",
},
]
export function FAQAccordion() {
return (
<div className="w-full max-w-2xl">
<h2 className="mb-6 text-2xl font-bold">Frequently Asked Questions</h2>
<Accordion type="single" collapsible>
{faqs.map((faq) => (
<AccordionItem key={faq.id} value={faq.id}>
<AccordionTrigger className="text-left">
{faq.question}
</AccordionTrigger>
<AccordionContent>{faq.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)
}Accordion with Icons
Add icons to accordion triggers:
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
import { HelpCircle, Zap, Lock } from "lucide-react"
export function AccordionWithIcons() {
return (
<Accordion type="single" collapsible>
<AccordionItem value="help">
<AccordionTrigger className="gap-2">
<HelpCircle className="h-5 w-5" />
Getting Help
</AccordionTrigger>
<AccordionContent>
Browse our documentation and support resources.
</AccordionContent>
</AccordionItem>
<AccordionItem value="performance">
<AccordionTrigger className="gap-2">
<Zap className="h-5 w-5" />
Performance
</AccordionTrigger>
<AccordionContent>
Learn optimization techniques for faster load times.
</AccordionContent>
</AccordionItem>
<AccordionItem value="security">
<AccordionTrigger className="gap-2">
<Lock className="h-5 w-5" />
Security
</AccordionTrigger>
<AccordionContent>
Understand our security practices and data protection.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Controlled Accordion
Manage accordion state programmatically:
"use client"
import { useState } from "react"
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
export function ControlledAccordion() {
const [openItems, setOpenItems] = useState<string[]>([])
const toggleItem = (value: string) => {
setOpenItems((prev) =>
prev.includes(value)
? prev.filter((item) => item !== value)
: [...prev, value],
)
}
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button
size="sm"
onClick={() => setOpenItems([])}
variant="outline"
>
Collapse All
</Button>
<Button
size="sm"
onClick={() => setOpenItems(["item-1", "item-2", "item-3"])}
variant="outline"
>
Expand All
</Button>
</div>
<Accordion
type="multiple"
value={openItems}
onValueChange={setOpenItems}
>
<AccordionItem value="item-1">
<AccordionTrigger>Item 1</AccordionTrigger>
<AccordionContent>Content for item 1</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Item 2</AccordionTrigger>
<AccordionContent>Content for item 2</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Item 3</AccordionTrigger>
<AccordionContent>Content for item 3</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)
}Accordion with Rich Content
Accordion items can contain any content:
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
export function AccordionWithRichContent() {
return (
<Accordion type="single" collapsible>
<AccordionItem value="code-example">
<AccordionTrigger>Code Example</AccordionTrigger>
<AccordionContent>
<pre className="rounded bg-slate-100 p-4">
{`const greeting = "Hello World"
console.log(greeting)`}
</pre>
</AccordionContent>
</AccordionItem>
<AccordionItem value="features">
<AccordionTrigger>Features</AccordionTrigger>
<AccordionContent>
<ul className="list-inside space-y-2">
<li>✓ Fully accessible</li>
<li>✓ Keyboard navigation</li>
<li>✓ Animated transitions</li>
</ul>
</AccordionContent>
</AccordionItem>
<AccordionItem value="action">
<AccordionTrigger>Take Action</AccordionTrigger>
<AccordionContent className="space-y-4">
<p>Ready to get started?</p>
<Button>Learn More</Button>
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Styled Accordion
Customize accordion appearance:
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/modules/cores/shadcn/components/ui/accordion"
export function StyledAccordion() {
return (
<Accordion type="single" collapsible className="w-full">
<AccordionItem
value="item-1"
className="border-l-4 border-l-blue-500"
>
<AccordionTrigger className="hover:text-blue-600">
Item 1
</AccordionTrigger>
<AccordionContent className="bg-blue-50">
Content for item 1
</AccordionContent>
</AccordionItem>
<AccordionItem
value="item-2"
className="border-l-4 border-l-green-500"
>
<AccordionTrigger className="hover:text-green-600">
Item 2
</AccordionTrigger>
<AccordionContent className="bg-green-50">
Content for item 2
</AccordionContent>
</AccordionItem>
</Accordion>
)
}API Reference
Accordion- Root componenttype-"single"(one open) or"multiple"(multiple open)collapsible- Allow closing open item (single mode only)value- Controlled open itemsonValueChange- Callback when open items changeAccordionItem- Individual accordion sectionvalue- Unique identifierAccordionTrigger- Clickable headerAccordionContent- Expandable content panel
Keyboard Navigation
- Enter/Space - Toggle open/closed
- ArrowDown - Move focus to next trigger
- ArrowUp - Move focus to previous trigger
- Home - Move focus to first trigger
- End - Move focus to last trigger
AlertDialog Component
Import AlertDialog components from @/modules/cores/shadcn/components/ui/alert-dialog:
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/modules/cores/shadcn/components/ui/alert-dialog"Installation
bunx --bun shadcn@latest add alert-dialogBasic Confirmation Dialog
Standard alert dialog for confirming actions:
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/modules/cores/shadcn/components/ui/alert-dialog"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
export function AlertDialogDemo() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Show Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Destructive Action Dialog
Dialog with destructive action button for delete operations:
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/modules/cores/shadcn/components/ui/alert-dialog"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
import { Trash2 } from "lucide-react"
export function AlertDialogDestructive() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete Chat</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete chat?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this chat conversation and all messages.
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Dialog with Custom Trigger
Custom element as trigger using asChild:
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/modules/cores/shadcn/components/ui/alert-dialog"
export function AlertDialogCustomTrigger() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="cursor-pointer text-blue-600 hover:underline">
Click here to confirm
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm action</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to proceed?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Yes, confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Programmatic Dialog Control
Control dialog visibility with state:
"use client"
import { useState } from "react"
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/modules/cores/shadcn/components/ui/alert-dialog"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
export function AlertDialogControlled() {
const [open, setOpen] = useState(false)
const handleConfirm = () => {
console.log("Confirmed")
setOpen(false)
}
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<Button>Open Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm action</AlertDialogTitle>
<AlertDialogDescription>
This is a controlled dialog component.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Components
AlertDialog
Root wrapper that manages dialog state. Accepts open and onOpenChange for controlled behavior.
AlertDialogTrigger
Trigger element that opens the dialog. Use asChild prop to apply dialog trigger to custom elements.
AlertDialogContent
Modal content wrapper. Handles stacking, animation, and backdrop.
AlertDialogHeader
Container for title and description. Typically styled with spacing.
AlertDialogFooter
Container for action buttons, typically right-aligned.
AlertDialogTitle
Semantic h2 heading for dialog title.
AlertDialogDescription
Descriptive text explaining the action being confirmed.
AlertDialogAction
Primary action button. Can accept variant="destructive" for delete operations.
AlertDialogCancel
Cancel button that closes dialog without action.
Props
// AlertDialog
interface AlertDialogProps {
open?: boolean
onOpenChange?: (open: boolean) => void
}
// AlertDialogAction
interface AlertDialogActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "destructive"
}Accessibility
- Dialog has
role="alertdialog"for screen readers - Keyboard navigation: Escape to cancel, Tab between buttons
- Focus management: Focuses first button on open, returns to trigger on close
- Title and description linked via
aria-labelledbyandaria-describedby
Best Practices
- Use for irreversible or high-impact actions only
- Keep title and description concise
- Explicitly name actions ("Delete" not "OK")
- Use destructive variant for delete/remove actions
- Always provide cancel option
- Avoid dialog chains or multiple dialogs
See Also
- Alert - Non-modal alert component
- Button - Action buttons
Alert Component
Import the Alert components from @/modules/cores/shadcn/components/ui/alert:
import { Alert, AlertTitle, AlertDescription } from "@/modules/cores/shadcn/components/ui/alert"
import { AlertCircle, Terminal } from "lucide-react"Installation
bunx --bun shadcn@latest add alertBasic Alert
Default variant with optional icon and title:
import { Alert, AlertTitle, AlertDescription } from "@/modules/cores/shadcn/components/ui/alert"
import { Terminal } from "lucide-react"
export function AlertDefault() {
return (
<Alert>
<Terminal className="h-4 w-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components and dependencies to your app using the cli.
</AlertDescription>
</Alert>
)
}Destructive Alert
Use the destructive variant for error or critical messages:
import { Alert, AlertTitle, AlertDescription } from "@/modules/cores/shadcn/components/ui/alert"
import { AlertCircle } from "lucide-react"
export function AlertDestructive() {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired. Please log in again.
</AlertDescription>
</Alert>
)
}Alert with Icons
Combine with lucide-react icons for visual emphasis:
import { Alert, AlertTitle, AlertDescription } from "@/modules/cores/shadcn/components/ui/alert"
import { AlertTriangle, Check, Info } from "lucide-react"
export function AlertWithIcons() {
return (
<>
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>Information</AlertTitle>
<AlertDescription>New updates are available.</AlertDescription>
</Alert>
<Alert className="border-yellow-500/50 text-yellow-700">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Warning</AlertTitle>
<AlertDescription>Please review your settings.</AlertDescription>
</Alert>
<Alert className="border-green-500/50 text-green-700">
<Check className="h-4 w-4" />
<AlertTitle>Success</AlertTitle>
<AlertDescription>Your changes have been saved.</AlertDescription>
</Alert>
</>
)
}Alert without Icon
Display alert without icon:
import { Alert, AlertTitle, AlertDescription } from "@/modules/cores/shadcn/components/ui/alert"
export function AlertNoIcon() {
return (
<Alert>
<AlertTitle>Notification</AlertTitle>
<AlertDescription>
This is a simple alert without an icon.
</AlertDescription>
</Alert>
)
}Props
Alert
interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "destructive"
}- variant:
"default"|"destructive"- Alert style variant - Extends standard HTML div attributes
AlertTitle
Semantic h5 element for alert heading:
interface AlertTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}AlertDescription
Wrapper for alert message content:
interface AlertDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}Styling
Customize alert appearance with className:
<Alert className="border-blue-500/50 bg-blue-50">
<AlertTitle>Custom Styled Alert</AlertTitle>
<AlertDescription>With custom colors and styling.</AlertDescription>
</Alert>Accessibility
- Alerts use
role="alert"for screen reader announcement - Icons are decorative; ensure title and description convey message
- Variant semantics automatically applied via CSS classes
Best Practices
- Use
destructivevariant for errors only - Keep descriptions concise and actionable
- Include icons that match message severity
- Avoid alert overload; use sparingly for important info
- Pair with appropriate color variants for clarity
See Also
- AlertDialog - Confirmation dialogs
- Button - Action buttons
AspectRatio Component
The AspectRatio component maintains a consistent aspect ratio for its content, preventing layout shift when media loads. It's essential for responsive image and video containers.
Installation
Install the AspectRatio component using the shadcn/ui CLI:
bunx --bun shadcn@latest add aspect-ratioBasic Usage
Image Container
Use AspectRatio to maintain consistent image dimensions:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function AspectRatioImageExample() {
return (
<AspectRatio ratio={16 / 9} className="bg-muted">
<Image
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80"
alt="Photo by Drew Beamer"
fill
className="rounded-md object-cover"
/>
</AspectRatio>
)
}Common Aspect Ratios
1:1 Square
Perfect for profile images and thumbnails:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function SquareAspectRatioExample() {
return (
<AspectRatio ratio={1 / 1} className="bg-muted">
<Image
src="https://images.unsplash.com/photo-1569163139394-de4798aa62b2?w=400&q=80"
alt="Profile"
fill
className="object-cover rounded-lg"
/>
</AspectRatio>
)
}4:3 Standard
Common for traditional video:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function StandardAspectRatioExample() {
return (
<AspectRatio ratio={4 / 3} className="bg-muted">
<Image
src="https://images.unsplash.com/photo-1634128221889-82ed6efcc547?w=600&q=80"
alt="Standard format"
fill
className="object-cover rounded-md"
/>
</AspectRatio>
)
}16:9 Widescreen
Most common for modern video and web content:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function WidescreenAspectRatioExample() {
return (
<AspectRatio ratio={16 / 9} className="bg-muted">
<Image
src="https://images.unsplash.com/photo-1611339555312-e607c249352d?w=800&q=80"
alt="Widescreen content"
fill
className="object-cover rounded-md"
/>
</AspectRatio>
)
}21:9 Ultrawide
For panoramic images:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function UltrawideAspectRatioExample() {
return (
<AspectRatio ratio={21 / 9} className="bg-muted">
<Image
src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1000&q=80"
alt="Panoramic view"
fill
className="object-cover rounded-md"
/>
</AspectRatio>
)
}Video Embeds
Embedded Video Player
Maintain aspect ratio for responsive video embeds:
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function EmbeddedVideoExample() {
return (
<AspectRatio ratio={16 / 9} className="bg-black">
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="YouTube video"
allowFullScreen
className="h-full w-full rounded-md"
/>
</AspectRatio>
)
}Vimeo Video
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function VimeoVideoExample() {
return (
<AspectRatio ratio={16 / 9} className="bg-black">
<iframe
src="https://player.vimeo.com/video/123456789"
title="Vimeo video"
allowFullScreen
className="h-full w-full rounded-md"
/>
</AspectRatio>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
ratio | number | 1 / 1 | Aspect ratio as width/height |
className | string | - | Container CSS classes |
children | ReactNode | - | Content to display |
Advanced Patterns
Responsive Grid of Images
Gallery with consistent aspect ratios:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
export function ImageGalleryExample() {
const images = [
"https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=300&q=80",
"https://images.unsplash.com/photo-1466891857616-5dba42b0e34c?w=300&q=80",
"https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=300&q=80",
]
return (
<div className="grid gap-4 grid-cols-3">
{images.map((src, i) => (
<AspectRatio key={i} ratio={1 / 1} className="bg-muted">
<Image
src={src}
alt={`Gallery image ${i + 1}`}
fill
className="object-cover rounded-md"
/>
</AspectRatio>
))}
</div>
)
}Dynamic Aspect Ratio
Calculate aspect ratio dynamically:
import Image from "next/image"
import { AspectRatio } from "@/modules/cores/shadcn/components/ui/aspect-ratio"
interface MediaProps {
src: string
width: number
height: number
alt: string
}
export function DynamicAspectRatioExample({ src, width, height, alt }: MediaProps) {
const ratio = width / height
return (
<AspectRatio ratio={ratio} className="bg-muted">
<Image
src={src}
alt={alt}
fill
className="object-cover rounded-md"
/>
</AspectRatio>
)
}Styling
Rounded Corners
// With rounded corners
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-lg">
<Image src="..." fill className="object-cover" />
</AspectRatio>With Border
// With border
<AspectRatio ratio={16 / 9} className="border-2 border-primary rounded-md">
<Image src="..." fill className="object-cover" />
</AspectRatio>Shadow Effect
// With shadow
<AspectRatio ratio={16 / 9} className="shadow-lg rounded-md overflow-hidden">
<Image src="..." fill className="object-cover" />
</AspectRatio>Best Practices
1. Always specify ratio - Never rely on default 1:1 ratio 2. Use with React Image - Combine with fill prop for optimization 3. Set object-fit - Use object-cover or object-contain for proper scaling 4. Prevent layout shift - AspectRatio prevents CLS (Cumulative Layout Shift) 5. Responsive sizes - Combine with responsive image srcset 6. Accessibility - Always include meaningful alt text
Common Aspect Ratio Values
| Use Case | Ratio | Value |
|---|---|---|
| Square | 1:1 | 1 / 1 |
| Portrait | 3:4 | 3 / 4 |
| Landscape | 4:3 | 4 / 3 |
| Widescreen | 16:9 | 16 / 9 |
| Ultrawide | 21:9 | 21 / 9 |
| Mobile | 9:16 | 9 / 16 |
| 1:1, 4:5 | 1 / 1, 4 / 5 | |
| YouTube Thumbnail | 16:9 | 16 / 9 |
Accessibility
- Use semantic
<figure>elements for images - Always provide descriptive
alttext - Ensure sufficient contrast with background
- Test video embeds with keyboard navigation
Avatar Component
Accessible avatar component that displays user images with fallback initials or placeholder text when images are unavailable.
Installation
bunx --bun shadcn-ui@latest add avatarBasic Avatar
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
export function BasicAvatar() {
return (
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
)
}Avatar with Initials
Display user initials as fallback:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
interface UserAvatarProps {
src?: string
name: string
}
export function UserAvatar({ src, name }: UserAvatarProps) {
// Extract initials from name
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)
return (
<Avatar>
{src && <AvatarImage src={src} />}
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
)
}Avatar Sizes
Create reusable avatar component with size variants:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
type AvatarSize = "sm" | "md" | "lg" | "xl"
interface SizedAvatarProps {
src?: string
name: string
size?: AvatarSize
}
export function SizedAvatar({
src,
name,
size = "md",
}: SizedAvatarProps) {
const sizeClasses: Record<AvatarSize, string> = {
sm: "h-8 w-8",
md: "h-10 w-10",
lg: "h-12 w-12",
xl: "h-16 w-16",
}
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
return (
<Avatar className={sizeClasses[size]}>
{src && <AvatarImage src={src} />}
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
)
}Avatar in List
Display avatars in a user list:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
interface User {
id: string
name: string
image?: string
role: string
}
interface UserListProps {
users: User[]
}
export function UserList({ users }: UserListProps) {
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
}
return (
<div className="space-y-4">
{users.map((user) => (
<div key={user.id} className="flex items-center gap-3">
<Avatar>
{user.image && <AvatarImage src={user.image} />}
<AvatarFallback>{getInitials(user.name)}</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.role}</p>
</div>
</div>
))}
</div>
)
}Avatar Group
Display multiple avatars stacked or side by side:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
interface AvatarGroupProps {
users: Array<{ id: string; name: string; image?: string }>
max?: number
}
export function AvatarGroup({ users, max = 3 }: AvatarGroupProps) {
const displayUsers = users.slice(0, max)
const remainingCount = users.length - max
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
}
return (
<div className="flex -space-x-2">
{displayUsers.map((user) => (
<div
key={user.id}
className="ring-2 ring-white"
>
<Avatar className="h-8 w-8">
{user.image && <AvatarImage src={user.image} />}
<AvatarFallback>{getInitials(user.name)}</AvatarFallback>
</Avatar>
</div>
))}
{remainingCount > 0 && (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 ring-2 ring-white text-xs font-semibold">
+{remainingCount}
</div>
)}
</div>
)
}Avatar with Status
Show online/offline status with avatar:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
type UserStatus = "online" | "offline" | "away"
interface UserWithStatusProps {
src?: string
name: string
status: UserStatus
}
export function AvatarWithStatus({
src,
name,
status,
}: UserWithStatusProps) {
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
}
const statusColors: Record<UserStatus, string> = {
online: "bg-green-500",
offline: "bg-gray-400",
away: "bg-yellow-500",
}
return (
<div className="relative inline-block">
<Avatar>
{src && <AvatarImage src={src} />}
<AvatarFallback>{getInitials(name)}</AvatarFallback>
</Avatar>
<div
className={`absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white ${statusColors[status]}`}
/>
</div>
)
}Avatar in Comment
Display avatar with comment text:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
interface Comment {
id: string
author: string
avatar?: string
text: string
timestamp: Date
}
interface CommentProps {
comment: Comment
}
export function CommentComponent({ comment }: CommentProps) {
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
}
const formatTime = (date: Date) => {
return new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(
Math.floor((date.getTime() - Date.now()) / 1000 / 60),
"minute",
)
}
return (
<div className="flex gap-3">
<Avatar className="h-8 w-8">
{comment.avatar && <AvatarImage src={comment.avatar} />}
<AvatarFallback>{getInitials(comment.author)}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="font-semibold">{comment.author}</p>
<p className="text-xs text-gray-500">
{formatTime(comment.timestamp)}
</p>
</div>
<p className="mt-1 text-sm">{comment.text}</p>
</div>
</div>
)
}Avatar with Badge
Add a badge overlay to avatar:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
interface AvatarWithBadgeProps {
src?: string
name: string
badgeLabel: string
}
export function AvatarWithBadge({
src,
name,
badgeLabel,
}: AvatarWithBadgeProps) {
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
}
return (
<div className="relative inline-block">
<Avatar className="h-12 w-12">
{src && <AvatarImage src={src} />}
<AvatarFallback>{getInitials(name)}</AvatarFallback>
</Avatar>
<Badge className="absolute -bottom-2 -right-2 text-xs">
{badgeLabel}
</Badge>
</div>
)
}Avatar in Team Card
Display avatar in team/organization cards:
import { Avatar, AvatarImage, AvatarFallback } from "@/modules/cores/shadcn/components/ui/avatar"
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/modules/cores/shadcn/components/ui/card"
interface TeamMember {
id: string
name: string
role: string
image?: string
}
interface TeamCardProps {
members: TeamMember[]
}
export function TeamCard({ members }: TeamCardProps) {
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
}
return (
<Card>
<CardHeader>
<CardTitle>Team Members</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{members.map((member) => (
<div key={member.id} className="flex items-center gap-3">
<Avatar>
{member.image && <AvatarImage src={member.image} />}
<AvatarFallback>{getInitials(member.name)}</AvatarFallback>
</Avatar>
<div>
<p className="font-medium text-sm">{member.name}</p>
<p className="text-xs text-gray-500">{member.role}</p>
</div>
</div>
))}
</CardContent>
</Card>
)
}API Reference
Avatar- Root containerclassName- Custom size classes (defaulth-10 w-10)AvatarImage- Image elementsrc- Image URLAvatarFallback- Fallback content when image fails to load- Text/initials content
Styling
- Default size:
h-10 w-10(40px) - Small:
h-8 w-8(32px) - Large:
h-12 w-12(48px) - Extra large:
h-16 w-16(64px)
Common Patterns
1. Initials Fallback - Extract first letter of first and last name 2. Status Indicator - Add colored dot for online/offline status 3. Avatar Stack - Group multiple avatars with negative margin 4. Avatar with Badge - Overlay badge for notifications or roles 5. Interactive Avatar - Make clickable for profile navigation
Badge Component
Simple, flexible badge component for displaying labels, tags, and status indicators.
Installation
bunx --bun shadcn-ui@latest add badgeDefault Badge
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
export function DefaultBadge() {
return <Badge>Badge</Badge>
}Badge Variants
All available variants with their use cases:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
export function BadgeVariants() {
return (
<div className="flex flex-wrap gap-2">
{/* Default variant - primary color */}
<Badge>Default</Badge>
{/* Secondary variant - muted background */}
<Badge variant="secondary">Secondary</Badge>
{/* Destructive variant - red for negative states */}
<Badge variant="destructive">Destructive</Badge>
{/* Outline variant - bordered */}
<Badge variant="outline">Outline</Badge>
</div>
)
}Badge with Icon
Combine badges with icons from lucide-react:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
import { CheckCircle, AlertCircle, Clock, Zap } from "lucide-react"
export function BadgeWithIcon() {
return (
<div className="space-y-4">
<div className="flex gap-2">
<Badge className="gap-1">
<CheckCircle className="h-3 w-3" />
Active
</Badge>
</div>
<div className="flex gap-2">
<Badge variant="secondary" className="gap-1">
<Clock className="h-3 w-3" />
Pending
</Badge>
</div>
<div className="flex gap-2">
<Badge variant="destructive" className="gap-1">
<AlertCircle className="h-3 w-3" />
Error
</Badge>
</div>
<div className="flex gap-2">
<Badge variant="outline" className="gap-1">
<Zap className="h-3 w-3" />
Premium
</Badge>
</div>
</div>
)
}Badge with Status Indicator
Use badges to show status with color coding:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
interface StatusBadgeProps {
status: "active" | "inactive" | "pending" | "error"
}
export function StatusBadge({ status }: StatusBadgeProps) {
const badgeConfig = {
active: { variant: "default" as const, label: "Active" },
inactive: { variant: "secondary" as const, label: "Inactive" },
pending: { variant: "secondary" as const, label: "Pending" },
error: { variant: "destructive" as const, label: "Error" },
}
const config = badgeConfig[status]
return <Badge variant={config.variant}>{config.label}</Badge>
}Badge as Link
Use asChild to render badge as a link:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
import { Link } from '@tanstack/react-router'
export function BadgeAsLink() {
return (
<div className="flex gap-2">
<Badge asChild>
<Link href="/docs/components">Documentation</Link>
</Badge>
<Badge asChild variant="outline">
<a href="https://github.com" target="_blank" rel="noopener noreferrer">
GitHub
</a>
</Badge>
</div>
)
}Badge in List
Display badges within lists for tagging:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
interface Post {
id: number
title: string
tags: string[]
}
interface PostListProps {
posts: Post[]
}
export function PostList({ posts }: PostListProps) {
return (
<div className="space-y-4">
{posts.map((post) => (
<div key={post.id} className="rounded border p-4">
<h3 className="mb-2 font-semibold">{post.title}</h3>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</div>
))}
</div>
)
}Dismissible Badge
Badge with close button for removable tags:
"use client"
import { useState } from "react"
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
import { X } from "lucide-react"
interface DismissibleBadgesProps {
initialTags: string[]
}
export function DismissibleBadges({
initialTags,
}: DismissibleBadgesProps) {
const [tags, setTags] = useState(initialTags)
const removeTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove))
}
return (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="cursor-pointer gap-1 pr-1"
>
{tag}
<X
className="h-3 w-3 hover:text-destructive"
onClick={() => removeTag(tag)}
/>
</Badge>
))}
</div>
)
}Badge in Card Header
Display badges within card headers for labeling:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/modules/cores/shadcn/components/ui/card"
export function CardWithBadges() {
return (
<Card>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>Feature Release</CardTitle>
<CardDescription>New features added to the platform</CardDescription>
</div>
<Badge>New</Badge>
</div>
</CardHeader>
<CardContent>
<p>Details about the feature release go here.</p>
</CardContent>
</Card>
)
}Badge Count
Display count badges for notifications:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
import { Bell } from "lucide-react"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
interface NotificationButtonProps {
count: number
}
export function NotificationButton({ count }: NotificationButtonProps) {
return (
<div className="relative inline-block">
<Button variant="outline" size="icon">
<Bell className="h-4 w-4" />
</Button>
{count > 0 && (
<Badge
className="absolute -right-2 -top-2 h-6 w-6 rounded-full flex items-center justify-center p-0 text-xs"
variant="destructive"
>
{count > 99 ? "99+" : count}
</Badge>
)}
</div>
)
}Badge Group
Display multiple badges together:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
interface BadgeGroupProps {
label: string
badges: string[]
variant?: "default" | "secondary" | "destructive" | "outline"
}
export function BadgeGroup({
label,
badges,
variant = "secondary",
}: BadgeGroupProps) {
return (
<div className="space-y-2">
<h4 className="text-sm font-medium">{label}</h4>
<div className="flex flex-wrap gap-2">
{badges.map((badge) => (
<Badge key={badge} variant={variant}>
{badge}
</Badge>
))}
</div>
</div>
)
}Animated Badge
Add hover effects to badges:
import { Badge } from "@/modules/cores/shadcn/components/ui/badge"
export function AnimatedBadge() {
return (
<div className="flex gap-2">
<Badge className="cursor-pointer transition-transform hover:scale-105">
Hover Me
</Badge>
<Badge
variant="outline"
className="cursor-pointer transition-colors hover:bg-slate-100"
>
Interactive
</Badge>
</div>
)
}API Reference
Badge- Root componentvariant-"default"|"secondary"|"destructive"|"outline"asChild- Render as child element (for Link, anchor)className- Custom CSS classes
Styling
Default CSS classes:
.h-4 .w-4- Icon size.gap-1- Icon spacing.rounded-full- Circular badge.flex- Flex container.items-center- Vertical centering
Common Patterns
1. Status Indicator - Use variant mapping for status colors 2. Removable Tags - Add X icon with click handler 3. Notification Badge - Position absolute over icon 4. Tag List - Use flex-wrap with gap for tag groups 5. Interactive Badge - Add hover effects with transitions
Breadcrumb
Breadcrumbs display the navigation hierarchy and current location within a site structure.
Basic Breadcrumb
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
export function BasicBreadcrumb() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/products">Products</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Laptops</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Breadcrumb with Custom Separator
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
import { ChevronRight, Slash } from 'lucide-react'
export function BreadcrumbCustomSeparator() {
return (
<div className="space-y-4">
{/* Chevron Separator */}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<ChevronRight className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Documentation</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<ChevronRight className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumbs</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/* Slash Separator */}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Documentation</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumbs</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
)
}Breadcrumb with Dropdown Menu
'use client'
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from '@/modules/cores/shadcn/components/ui/dropdown-menu'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
import { ChevronDown } from 'lucide-react'
export function BreadcrumbWithDropdown() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 px-2 gap-1">
Components
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>
<a href="/components/alerts">Alerts</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a href="/components/buttons">Buttons</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a href="/components/cards">Cards</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a href="/components/dropdowns">Dropdowns</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumbs</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Breadcrumb with Icons
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
import { Home, FileText, Code } from 'lucide-react'
export function BreadcrumbWithIcons() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" className="flex items-center gap-2">
<Home className="h-4 w-4" />
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs" className="flex items-center gap-2">
<FileText className="h-4 w-4" />
Documentation
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage className="flex items-center gap-2">
<Code className="h-4 w-4" />
Code Examples
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}Dynamic Breadcrumb from Route
'use client'
import { usePathname } from 'next/navigation'
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
interface BreadcrumbItem {
href: string
label: string
}
function generateBreadcrumbs(pathname: string): BreadcrumbItem[] {
const segments = pathname.split('/').filter(Boolean)
const breadcrumbs: BreadcrumbItem[] = [
{ href: '/', label: 'Home' },
]
let path = ''
segments.forEach((segment) => {
path += `/${segment}`
breadcrumbs.push({
href: path,
label: segment.charAt(0).toUpperCase() + segment.slice(1),
})
})
return breadcrumbs
}
export function DynamicBreadcrumb() {
const pathname = usePathname()
const breadcrumbs = generateBreadcrumbs(pathname)
const lastBreadcrumb = breadcrumbs[breadcrumbs.length - 1]
return (
<Breadcrumb>
<BreadcrumbList>
{breadcrumbs.map((item, index) => (
<div key={item.href} className="flex items-center gap-1">
{index > 0 && <BreadcrumbSeparator />}
<BreadcrumbItem>
{item === lastBreadcrumb ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
) : (
<BreadcrumbLink href={item.href}>
{item.label}
</BreadcrumbLink>
)}
</BreadcrumbItem>
</div>
))}
</BreadcrumbList>
</Breadcrumb>
)
}Collapsible Breadcrumb (for long paths)
'use client'
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
import { MoreHorizontal } from 'lucide-react'
export function CollapsibleBreadcrumb() {
const breadcrumbs = [
{ href: '/', label: 'Home' },
{ href: '/projects', label: 'Projects' },
{ href: '/projects/acme', label: 'Acme Corp' },
{ href: '/projects/acme/dashboard', label: 'Dashboard' },
{ href: '/projects/acme/dashboard/analytics', label: 'Analytics' },
{ href: '/projects/acme/dashboard/analytics/reports', label: 'Reports' },
]
const hiddenItems = breadcrumbs.slice(2, -2)
const visibleItems = [
...breadcrumbs.slice(0, 2),
...breadcrumbs.slice(-2),
]
return (
<Breadcrumb>
<BreadcrumbList>
{visibleItems.map((item, index, array) => {
const isFirst = index === 0
const isBeforeEllipsis = index === 1
const showEllipsis = hiddenItems.length > 0 && isBeforeEllipsis
const isLast = item === array[array.length - 1]
return (
<div key={item.href}>
{!isFirst && <BreadcrumbSeparator />}
{showEllipsis && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{hiddenItems.map((hiddenItem) => (
<DropdownMenuItem key={hiddenItem.href} asChild>
<a href={hiddenItem.href}>
{hiddenItem.label}
</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<BreadcrumbSeparator />
</>
)}
<BreadcrumbItem>
{isLast ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
) : (
<BreadcrumbLink href={item.href}>
{item.label}
</BreadcrumbLink>
)}
</BreadcrumbItem>
</div>
)
})}
</BreadcrumbList>
</Breadcrumb>
)
}Breadcrumb with JSON Schema
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage,
} from '@/modules/cores/shadcn/components/ui/breadcrumb'
export function BreadcrumbWithSchema() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://example.com',
},
{
'@type': 'ListItem',
position: 2,
name: 'Products',
item: 'https://example.com/products',
},
{
'@type': 'ListItem',
position: 3,
name: 'Laptops',
item: 'https://example.com/products/laptops',
},
],
}),
}}
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/products">Products</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Laptops</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</>
)
}Key Components
| Component | Purpose |
|---|---|
Breadcrumb | Root container |
BreadcrumbList | Container for breadcrumb items |
BreadcrumbItem | Individual breadcrumb entry |
BreadcrumbLink | Clickable navigation link |
BreadcrumbSeparator | Visual divider between items |
BreadcrumbPage | Current/last page (not clickable) |
Common Patterns
Pattern: Basic Navigation
- Home → Category → Subcategory → Current Page
- Each level is a link except the last
Pattern: With Dropdown
- Use dropdown for long hierarchies
- Show first level and current page normally
- Hidden levels in dropdown
Pattern: Dynamic from Route
- Parse pathname to generate breadcrumbs
- Auto-capitalize segment names
- Last segment as BreadcrumbPage
Pattern: Schema Markup
- Add JSON-LD for SEO
- BreadcrumbList with itemListElement
- Improves search engine understanding
Accessibility
- Semantic HTML with nav element
- Links have proper href attributes
- Current page indicated (not clickable)
- Screen reader friendly
- Keyboard navigation support
Best Practices
1. Current Page: Last item should be current page (BreadcrumbPage, not link) 2. Hierarchy: Display actual site hierarchy, not browsing history 3. Clarity: Use clear, concise labels 4. Separators: Keep consistent separator style throughout 5. Mobile: Consider collapsible breadcrumbs on small screens 6. SEO: Add schema.org markup for structured data
Button Component
Installation
bunx shadcn-ui@latest add buttonCreates: @/modules/cores/shadcn/components/ui/button.tsx
Basic Usage
import { Button } from '@/modules/cores/shadcn/components/ui/button'
export function BasicButton() {
return <Button>Click me</Button>
}Variants
Default (Primary)
<Button>Default button</Button>Styling: Solid background, white text, rounded corners
Secondary
<Button variant="secondary">Secondary button</Button>Styling: Light gray background, dark text
Destructive
<Button variant="destructive">Delete item</Button>Styling: Red background, white text (danger action)
Outline
<Button variant="outline">Outline button</Button>Styling: Border only, transparent background
Ghost
<Button variant="ghost">Ghost button</Button>Styling: No border or background, hover effect only
Link
<Button variant="link">Link button</Button>Styling: Underlined text, no background (use for inline links)
Sizes
Small
<Button size="sm">Small button</Button>Default
<Button size="default">Default button</Button>(Omit size prop for default)
Large
<Button size="lg">Large button</Button>Icon
import { Plus } from '@/modules/cores/shadcn/components/icons'
<Button size="icon">
<Plus className="w-4 h-4" />
</Button>Square button for icons only
Common Combinations
// Primary action
<Button>Save changes</Button>
// Secondary action
<Button variant="secondary">Cancel</Button>
// Danger action
<Button variant="destructive">Delete</Button>
// Tertiary action
<Button variant="ghost">More options</Button>
// Link-like button
<Button variant="link">Learn more</Button>
// Icon button
<Button size="icon" variant="ghost">
<Settings className="w-4 h-4" />
</Button>
// Small outline
<Button size="sm" variant="outline">Copy</Button>Advanced Patterns
asChild Pattern (Compose with Link)
For React navigation, use asChild to render as Link:
import { Link } from '@tanstack/react-router'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
export function NavButton() {
return (
<Button asChild>
<Link href="/dashboard">Go to dashboard</Link>
</Button>
)
}What it does: Button renders as <a> tag via Link component
Use when:
- Navigation between pages
- External links with button styling
- Want button accessibility + link semantics
// Multiple children work too
<Button asChild>
<a href="https://example.com">
<span>Open external site</span>
<ExternalLink className="w-4 h-4 ml-2" />
</a>
</Button>Loading State Pattern
'use client'
import { useState } from 'react'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
import { Loader } from '@/modules/cores/shadcn/components/icons'
export function LoadingButton() {
const [isLoading, setIsLoading] = useState(false)
const handleClick = async () => {
setIsLoading(true)
try {
// Your async action
await fetch('/api/submit', { method: 'POST' })
} finally {
setIsLoading(false)
}
}
return (
<Button onClick={handleClick} disabled={isLoading}>
{isLoading && (
<Loader className="w-4 h-4 mr-2 animate-spin" />
)}
{isLoading ? 'Submitting...' : 'Submit'}
</Button>
)
}Pattern breakdown: 1. Track loading state with useState 2. Disable button while loading 3. Show spinner icon (animated) 4. Change text to show status
Button Group Pattern
export function ButtonGroup() {
return (
<div className="flex gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</div>
)
}CSS:
flex gap-2for spacing- Use variant contrast (outline + solid)
Button with Icon and Text
import { Plus } from '@/modules/cores/shadcn/components/icons'
export function CreateButton() {
return (
<Button>
<Plus className="w-4 h-4 mr-2" />
Create new item
</Button>
)
}Conditional Rendering
export function ContextualButton({ isEditing }: { isEditing: boolean }) {
return (
<Button
variant={isEditing ? 'destructive' : 'default'}
onClick={() => {
// handle toggle
}}
>
{isEditing ? 'Cancel edit' : 'Edit'}
</Button>
)
}Full Width Button
<Button className="w-full">Full width button</Button>Add w-full class for 100% width
Button in Form Context
import { Button } from '@/modules/cores/shadcn/components/ui/button'
export function SignUpForm() {
return (
<form>
{/* form fields */}
<Button type="submit" className="w-full">
Sign up
</Button>
<Button type="reset" variant="outline" className="w-full">
Clear
</Button>
</form>
)
}Important: Use type="submit" for form submission
Accessibility
ARIA Labels
For icon-only buttons, always add aria-label:
<Button size="icon" aria-label="Delete item">
<Trash2 className="w-4 h-4" />
</Button>Disabled State
<Button disabled>Disabled button</Button>Automatically:
- Shows gray styling
- Prevents click handlers
- Adds
aria-disabled="true"
Focus Management
Buttons automatically handle focus for keyboard navigation.
Test with Tab key: Button should show focus outline.
Styling & Customization
Tailwind Classes
Combine Button variants with Tailwind:
// Larger button with padding
<Button className="px-6 py-3 text-lg">Large custom</Button>
// Full width with margin
<Button className="w-full mb-4">Spaced button</Button>
// Custom color (override variant)
<Button className="bg-purple-600 hover:bg-purple-700">
Custom color
</Button>Component Props
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'
size?: 'default' | 'sm' | 'lg' | 'icon'
asChild?: boolean
}All standard HTML button attributes work:
onClickdisabledtype="submit" | "button" | "reset"classNamearia-*
Examples by Use Case
Primary Action (Save)
<Button>Save changes</Button>Secondary Action (Cancel)
<Button variant="secondary">Cancel</Button>Dangerous Action (Delete)
<Button variant="destructive" onClick={handleDelete}>
Delete account
</Button>Navigation Link
<Button asChild>
<Link href="/profile">View profile</Link>
</Button>Form Submission with Loading
<Button
type="submit"
disabled={isLoading}
onClick={handleSubmit}
>
{isLoading ? 'Saving...' : 'Save'}
</Button>Toggle Button
<Button
variant={isActive ? 'default' : 'outline'}
onClick={() => setIsActive(!isActive)}
>
{isActive ? 'Active' : 'Inactive'}
</Button>Icon + Text Action
<Button>
<Download className="w-4 h-4 mr-2" />
Download report
</Button>Type Safety
Full TypeScript support:
import type { ButtonHTMLAttributes } from 'react'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
interface CustomButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean
icon?: React.ReactNode
}
export function CustomButton({
loading,
icon,
children,
...props
}: CustomButtonProps) {
return (
<Button {...props} disabled={loading}>
{loading && <Loader className="w-4 h-4 mr-2 animate-spin" />}
{icon && <span className="mr-2">{icon}</span>}
{children}
</Button>
)
}Related Components
- Input - Text input fields
- Card - Container layout
- Form - Form management
Related Patterns
- Dialog - Modal with actions
- Dropdown Menu - Button-triggered menu
- Toast - Feedback after button click
Calendar
Calendar component provides date selection functionality with support for single dates and date ranges.
Basic Calendar
'use client'
import { useState } from 'react'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
export function BasicCalendar() {
const [date, setDate] = useState<Date | undefined>(new Date())
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-md border"
/>
)
}Calendar with Popover (Date Picker)
'use client'
import { useState } from 'react'
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/modules/cores/shadcn/components/ui/popover'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
import { cn } from '@/modules/cores/lib/utils'
export function DatePicker() {
const [date, setDate] = useState<Date | undefined>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'w-[240px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, 'PPP') : 'Pick a date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
disabled={(date) =>
date > new Date() || date < new Date('1900-01-01')
}
initialFocus
/>
</PopoverContent>
</Popover>
)
}Date Range Picker
'use client'
import { useState } from 'react'
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/modules/cores/shadcn/components/ui/popover'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
import { cn } from '@/modules/cores/lib/utils'
interface DateRange {
from?: Date
to?: Date
}
export function DateRangePicker() {
const [dateRange, setDateRange] = useState<DateRange | undefined>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'w-[300px] justify-start text-left font-normal',
!dateRange && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange?.from ? (
dateRange.to ? (
<>
{format(dateRange.from, 'LLL dd, y')} -{' '}
{format(dateRange.to, 'LLL dd, y')}
</>
) : (
format(dateRange.from, 'LLL dd, y')
)
) : (
'Pick a date range'
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange?.from}
selected={dateRange}
onSelect={setDateRange}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
)
}Calendar with Disabled Dates
'use client'
import { useState } from 'react'
import { format, isBefore, startOfToday } from 'date-fns'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/modules/cores/shadcn/components/ui/popover'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
export function CalendarWithDisabledDates() {
const [date, setDate] = useState<Date | undefined>()
// Disable past dates
const disabledDates = (date: Date) => {
return isBefore(date, startOfToday())
}
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
{date ? format(date, 'PPP') : 'Select date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
disabled={disabledDates}
initialFocus
/>
</PopoverContent>
</Popover>
)
}Multiple Date Selection
'use client'
import { useState } from 'react'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
export function MultiDatePicker() {
const [dates, setDates] = useState<Date[]>([])
const handleSelect = (date: Date | undefined) => {
if (!date) return
const index = dates.findIndex(
(d) => d.toDateString() === date.toDateString()
)
if (index > -1) {
setDates(dates.filter((_, i) => i !== index))
} else {
setDates([...dates, date])
}
}
const selectedDates = dates.map((d) => d.toDateString())
return (
<>
<Calendar
mode="single"
selected={undefined}
onSelect={handleSelect}
className="rounded-md border"
modifiers={{
selected: dates,
}}
modifiersClassNames={{
selected: 'bg-primary text-primary-foreground',
}}
/>
<div className="mt-4 space-y-2">
<h3 className="font-semibold">Selected Dates:</h3>
<ul className="space-y-1">
{dates
.sort((a, b) => a.getTime() - b.getTime())
.map((date) => (
<li key={date.toISOString()} className="text-sm">
{date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</li>
))}
</ul>
</div>
</>
)
}Month and Year Picker
'use client'
import { useState } from 'react'
import { format } from 'date-fns'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
export function MonthYearPicker() {
const [date, setDate] = useState<Date | undefined>(new Date())
const [mode, setMode] = useState<'days' | 'months' | 'years'>('days')
return (
<div className="space-y-4">
<div className="flex gap-2">
<button
onClick={() => setMode('days')}
className={`px-4 py-2 rounded ${mode === 'days' ? 'bg-primary text-white' : 'bg-gray-200'}`}
>
Days
</button>
<button
onClick={() => setMode('months')}
className={`px-4 py-2 rounded ${mode === 'months' ? 'bg-primary text-white' : 'bg-gray-200'}`}
>
Months
</button>
<button
onClick={() => setMode('years')}
className={`px-4 py-2 rounded ${mode === 'years' ? 'bg-primary text-white' : 'bg-gray-200'}`}
>
Years
</button>
</div>
{mode === 'days' && (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-md border"
/>
)}
{mode === 'months' && date && (
<div className="p-4">
<p className="mb-4 text-center font-semibold">
Select Month for {date.getFullYear()}
</p>
<div className="grid grid-cols-3 gap-2">
{Array.from({ length: 12 }, (_, i) => {
const monthDate = new Date(date.getFullYear(), i, 1)
return (
<button
key={i}
onClick={() => setDate(monthDate)}
className="py-2 px-3 rounded hover:bg-primary hover:text-white"
>
{format(monthDate, 'MMM')}
</button>
)
})}
</div>
</div>
)}
{mode === 'years' && (
<div className="p-4">
<p className="mb-4 text-center font-semibold">
Select Year
</p>
<div className="grid grid-cols-3 gap-2">
{Array.from({ length: 20 }, (_, i) => {
const year = new Date().getFullYear() - 10 + i
return (
<button
key={year}
onClick={() => setDate(new Date(year, 0, 1))}
className="py-2 px-3 rounded hover:bg-primary hover:text-white"
>
{year}
</button>
)
})}
</div>
</div>
)}
</div>
)
}Calendar in Form
'use client'
import { useState } from 'react'
import { format } from 'date-fns'
import { Calendar } from '@/modules/cores/shadcn/components/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/modules/cores/shadcn/components/ui/popover'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
import { Input } from '@/modules/cores/shadcn/components/ui/input'
import { Label } from '@/modules/cores/shadcn/components/ui/label'
interface FormData {
name: string
birthDate?: Date
eventDate?: Date
}
export function CalendarForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log('Form submitted:', formData)
}
return (
<form onSubmit={handleSubmit} className="space-y-6 w-full max-w-md">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder="Enter your name"
/>
</div>
<div className="space-y-2">
<Label>Birth Date</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
{formData.birthDate
? format(formData.birthDate, 'PPP')
: 'Pick a date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.birthDate}
onSelect={(date) =>
setFormData({ ...formData, birthDate: date })
}
disabled={(date) => date > new Date()}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label>Event Date</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
{formData.eventDate
? format(formData.eventDate, 'PPP')
: 'Pick a date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.eventDate}
onSelect={(date) =>
setFormData({ ...formData, eventDate: date })
}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<Button type="submit">Submit</Button>
</form>
)
}Key Props
| Prop | Type | Description |
|---|---|---|
mode | `'single' \ | 'range' \ |
selected | `Date \ | DateRange \ |
onSelect | (date) => void | Callback when date is selected |
disabled | (date: Date) => boolean | Function to disable specific dates |
numberOfMonths | number | Show multiple months (for range) |
initialFocus | boolean | Auto focus calendar on mount |
defaultMonth | Date | Initial month to display |
Dependencies
Install required packages:
npm install date-fnsThe calendar uses:
date-fnsfor date manipulationreact-day-pickerfor the calendar UI (included in shadcn)
Common Patterns
Pattern: Date Picker
- Popover with Calendar inside
- Single date selection
- Button shows formatted date
Pattern: Date Range
- Set
mode="range" - Use
numberOfMonths={2}for dual calendar view - Handle
fromandtodates
Pattern: Booking System
- Disable past dates
- Disable unavailable dates
- Show selected dates clearly
Pattern: Form Integration
- Embed calendar in form
- Store selected date in state
- Submit as part of form data
Accessibility
- Keyboard navigation (arrow keys)
- Tab to navigate months
- Screen reader support
- Focus management
- ARIA labels and descriptions
Best Practices
1. Disable Past Dates: Use for bookings and future events 2. Multiple Calendars: Use numberOfMonths for date ranges 3. Format Display: Use date-fns format for consistent formatting 4. Clear Selection: Show selected dates prominently 5. Validation: Validate date ranges on submission 6. Performance: Memoize disabled date function for performance
Card Component
Installation
bunx shadcn-ui@latest add cardCreates: @/modules/cores/shadcn/components/ui/card.tsx
Basic Usage
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/modules/cores/shadcn/components/ui/card'
export function BasicCard() {
return (
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
</CardHeader>
<CardContent>
<p>This is the card content.</p>
</CardContent>
</Card>
)
}Card Structure
A complete Card has these optional parts:
<Card>
<CardHeader>
<CardTitle>Main heading</CardTitle>
<CardDescription>Subtitle or description</CardDescription>
</CardHeader>
<CardContent>
{/* Main content goes here */}
</CardContent>
<CardFooter>
{/* Footer actions or info */}
</CardFooter>
</Card>CardHeader
Top section, typically for title and description:
<CardHeader>
<CardTitle>Settings</CardTitle>
<CardDescription>Manage your account preferences</CardDescription>
</CardHeader>CardTitle
Main heading, usually inside CardHeader:
<CardTitle>Profile Information</CardTitle>CardDescription
Subtitle or helper text:
<CardDescription>Update your profile details and preferences</CardDescription>CardContent
Main content area (middle section):
<CardContent>
<p>Your content here</p>
</CardContent>CardFooter
Bottom section for actions or info:
<CardFooter className="flex gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>Common Patterns
Card with Form
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
CardFooter,
} from '@/modules/cores/shadcn/components/ui/card'
import { Input } from '@/modules/cores/shadcn/components/ui/input'
import { Label } from '@/modules/cores/shadcn/components/ui/label'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
export function ProfileCard() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Edit Profile</CardTitle>
<CardDescription>Update your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="John Doe" />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="john@example.com" />
</div>
</CardContent>
<CardFooter className="flex gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save changes</Button>
</CardFooter>
</Card>
)
}Pattern breakdown:
max-w-mdfor constrained widthCardHeaderwith title and descriptionCardContentwithspace-y-4for vertical spacing- Each form field in
space-y-2wrapper CardFooterwith two buttons
Card Grid Layout
Multiple cards in responsive grid:
export function CardGrid() {
const items = [
{ id: 1, title: 'Item 1', description: 'Description 1' },
{ id: 2, title: 'Item 2', description: 'Description 2' },
{ id: 3, title: 'Item 3', description: 'Description 3' },
]
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map((item) => (
<Card key={item.id}>
<CardHeader>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{item.description}</p>
</CardContent>
</Card>
))}
</div>
)
}Grid pattern:
grid-cols-1on mobile (1 column)md:grid-cols-2on tablets (2 columns)lg:grid-cols-3on desktop (3 columns)gap-4for spacing between cards
Card with Header Action
Add button or icon to header:
import { MoreVertical } from '@/modules/cores/shadcn/components/icons'
import { Button } from '@/modules/cores/shadcn/components/ui/button'
export function CardWithAction() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle>Total Revenue</CardTitle>
<CardDescription>From the last 30 days</CardDescription>
</div>
<Button variant="ghost" size="icon">
<MoreVertical className="w-4 h-4" />
</Button>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">$45,231.89</p>
</CardContent>
</Card>
)
}Header action pattern:
- Use
flex flex-row items-center justify-between - Left side: title and description
- Right side: action button
Card List Item
Card for individual list items:
export function CardListItem() {
const items = [
{ id: 1, name: 'Alice', role: 'Developer' },
{ id: 2, name: 'Bob', role: 'Designer' },
]
return (
<div className="space-y-2">
{items.map((item) => (
<Card key={item.id}>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold">{item.name}</p>
<p className="text-sm text-gray-500">{item.role}</p>
</div>
<Button variant="ghost">Edit</Button>
</div>
</CardContent>
</Card>
))}
</div>
)
}Dashboard Widget Card
Card for metrics or statistics:
import { TrendingUp } from '@/modules/cores/shadcn/components/icons'
export function DashboardCard() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div>
<CardDescription>Monthly Revenue</CardDescription>
<CardTitle className="text-4xl">$12,500</CardTitle>
</div>
<TrendingUp className="w-8 h-8 text-green-500" />
</CardHeader>
<CardContent>
<p className="text-xs text-gray-500">+12% from last month</p>
</CardContent>
</Card>
)
}Card with Image
Card with image header:
import Image from 'next/image'
export function CardWithImage() {
return (
<Card className="overflow-hidden">
<div className="relative w-full h-48">
<Image
src="/hero.jpg"
alt="Card image"
fill
className="object-cover"
/>
</div>
<CardHeader>
<CardTitle>Featured Article</CardTitle>
</CardHeader>
<CardContent>
<p>This is the article summary and description.</p>
</CardContent>
<CardFooter>
<Button className="w-full">Read more</Button>
</CardFooter>
</Card>
)
}Image pattern:
- Use
overflow-hiddenon Card to clip image - Image with
relative,w-full,h-48 - Use React Image component for optimization
fillandobject-coverfor proper sizing
Nested Cards (Section Groups)
Group related cards:
export function NestedCardGroup() {
return (
<Card>
<CardHeader>
<CardTitle>Account Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Card className="bg-gray-50">
<CardContent className="pt-6">
<h3 className="font-semibold mb-2">Security</h3>
<p className="text-sm text-gray-600">
Manage your password and login methods
</p>
</CardContent>
</Card>
<Card className="bg-gray-50">
<CardContent className="pt-6">
<h3 className="font-semibold mb-2">Privacy</h3>
<p className="text-sm text-gray-600">
Control your data and visibility
</p>
</CardContent>
</Card>
</CardContent>
</Card>
)
}Card with Divider
Separate sections inside card:
export function CardWithDivider() {
return (
<Card>
<CardHeader>
<CardTitle>Order #1234</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-sm font-semibold">Items</p>
<p className="text-sm text-gray-600">2 × Product Name</p>
</div>
<div className="border-t pt-4">
<p className="text-sm font-semibold">Total</p>
<p className="text-xl font-bold">$99.99</p>
</div>
</CardContent>
</Card>
)
}Styling & Customization
Card Size
// Small card
<Card className="w-64">...</Card>
// Medium card (default)
<Card className="w-96">...</Card>
// Large card
<Card className="max-w-2xl">...</Card>
// Full width
<Card className="w-full">...</Card>Card Background Color
// Default (white)
<Card>...</Card>
// Light gray
<Card className="bg-gray-50">...</Card>
// Custom color
<Card className="bg-blue-50">...</Card>Card Spacing
// Compact (default)
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>Content</CardContent>
</Card>
// Generous spacing
<Card className="p-8">
<CardHeader className="pb-8">
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>Content</CardContent>
</Card>Card Border & Shadow
// Default (light border, subtle shadow)
<Card>...</Card>
// No shadow
<Card className="shadow-none border">...</Card>
// Heavy shadow
<Card className="shadow-lg">...</Card>
// Hover effect
<Card className="cursor-pointer hover:shadow-lg transition-shadow">...</Card>Props & Structure
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
interface CardDescriptionProps extends React.HTMLAttributes<HTMLDivElement> {}
interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {}
interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}All components accept standard HTML attributes:
classNameidstyleonClickdata-*attributes
Accessibility
Semantic Structure
<Card>
<CardHeader>
<CardTitle>
Heading for screen readers
</CardTitle>
</CardHeader>
<CardContent>
<p>Semantic paragraph content</p>
</CardContent>
</Card>ARIA Labels
For icon-only cards:
<Card aria-label="User profile card">
{/* content */}
</Card>Focus Management
Cards are semantic containers; interactive elements inside handle focus:
<Card>
<CardContent>
<Button>Focusable button</Button>
<Input />
</CardContent>
</Card>Type Safety
import type { HTMLAttributes } from 'react'
import { Card, CardContent } from '@/modules/cores/shadcn/components/ui/card'
interface CustomCardProps extends HTMLAttributes<HTMLDivElement> {
title: string
description?: string
}
export function CustomCard({
title,
description,
children,
...props
}: CustomCardProps) {
return (
<Card {...props}>
<CardHeader>
<CardTitle>{title}</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
)
}Related Components
Carousel Component
The Carousel component provides a feature-rich carousel/slider built on Embla Carousel. It supports touch gestures, keyboard navigation, plugins, and responsive layouts.
Installation
Install the Carousel component using the shadcn/ui CLI:
bunx --bun shadcn@latest add carouselBasic Usage
Simple Carousel
Create a basic carousel with navigation buttons:
import * as React from "react"
import { Card, CardContent } from "@/modules/cores/shadcn/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
export function CarouselExample() {
return (
<Carousel>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Components
Carousel
Main carousel container.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
opts | EmblaOptionsType | - | Embla options (alignment, loop, etc.) |
plugins | EmblaPluginType[] | - | Array of Embla plugins |
className | string | - | Container CSS classes |
orientation | `"horizontal" \ | "vertical"` | - |
CarouselContent
Wrapper for carousel items.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Content CSS classes |
CarouselItem
Individual slide item.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Item CSS classes |
CarouselPrevious / CarouselNext
Navigation buttons for previous/next slides.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Button CSS classes |
variant | string | - | Button variant style |
Autoplay Plugin
Basic Autoplay
Enable automatic slide rotation:
"use client"
import * as React from "react"
import Autoplay from "embla-carousel-autoplay"
import { Card, CardContent } from "@/modules/cores/shadcn/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
export function CarouselAutoplayExample() {
const plugin = React.useRef(
Autoplay({ delay: 2000, stopOnInteraction: true })
)
return (
<Carousel plugins={[plugin.current]}>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Responsive Layouts
Multi-Column Carousel
Display multiple items per slide on different screen sizes:
import * as React from "react"
import { Card, CardContent } from "@/modules/cores/shadcn/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
export function ResponsiveCarouselExample() {
return (
<Carousel
opts={{
align: "start",
}}
className="w-full max-w-xs"
>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3">
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Custom Item Spacing
Control spacing between carousel items:
import * as React from "react"
import { Card, CardContent } from "@/modules/cores/shadcn/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
export function SpacedCarouselExample() {
return (
<Carousel
opts={{
align: "start",
}}
className="w-full max-w-sm"
>
<CarouselContent className="-ml-1">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index} className="pl-1 md:basis-1/2 lg:basis-1/3">
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Advanced Patterns
Image Gallery Carousel
Full-featured image carousel:
"use client"
import * as React from "react"
import Image from "next/image"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
interface GalleryImage {
id: string
src: string
alt: string
}
export function ImageGalleryCarouselExample() {
const images: GalleryImage[] = [
{ id: "1", src: "https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=800&q=80", alt: "Image 1" },
{ id: "2", src: "https://images.unsplash.com/photo-1466891857616-5dba42b0e34c?w=800&q=80", alt: "Image 2" },
{ id: "3", src: "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=800&q=80", alt: "Image 3" },
]
return (
<Carousel className="w-full max-w-2xl">
<CarouselContent>
{images.map((image) => (
<CarouselItem key={image.id}>
<div className="relative w-full aspect-video">
<Image
src={image.src}
alt={image.alt}
fill
className="object-cover rounded-lg"
/>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Product Showcase Carousel
Carousel for product display:
"use client"
import * as React from "react"
import Image from "next/image"
import { Button } from "@/modules/cores/shadcn/components/ui/button"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
interface Product {
id: string
name: string
price: number
image: string
}
export function ProductCarouselExample() {
const products: Product[] = [
{ id: "1", name: "Product 1", price: 99.99, image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&q=80" },
{ id: "2", name: "Product 2", price: 149.99, image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&q=80" },
{ id: "3", name: "Product 3", price: 179.99, image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&q=80" },
]
return (
<Carousel className="w-full max-w-md">
<CarouselContent>
{products.map((product) => (
<CarouselItem key={product.id}>
<div className="space-y-4">
<div className="relative w-full aspect-square bg-muted rounded-lg overflow-hidden">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover"
/>
</div>
<div>
<h3 className="font-semibold">{product.name}</h3>
<p className="text-lg font-bold mt-2">${product.price}</p>
<Button className="w-full mt-2">Add to Cart</Button>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Testimonials Carousel
Carousel for displaying testimonials:
"use client"
import * as React from "react"
import Autoplay from "embla-carousel-autoplay"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/modules/cores/shadcn/components/ui/carousel"
interface Testimonial {
id: string
text: string
author: string
role: string
}
export function TestimonialsCarouselExample() {
const plugin = React.useRef(
Autoplay({ delay: 4000, stopOnInteraction: true })
)
const testimonials: Testimonial[] = [
{ id: "1", text: "Great product, highly recommend!", author: "John Doe", role: "CEO" },
{ id: "2", text: "Excellent service and support.", author: "Jane Smith", role: "Manager" },
{ id: "3", text: "Best solution we found in the market.", author: "Bob Johnson", role: "Developer" },
]
return (
<Carousel plugins={[plugin.current]} className="w-full max-w-2xl">
<CarouselContent>
{testimonials.map((testimonial) => (
<CarouselItem key={testimonial.id}>
<div className="p-8 rounded-lg border bg-card">
<p className="text-lg italic mb-4">"{testimonial.text}"</p>
<div>
<p className="font-semibold">{testimonial.author}</p>
<p className="text-sm text-muted-foreground">{testimonial.role}</p>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Configuration Options
Embla Options
const opts = {
align: "start", // "start", "center", "end"
loop: true, // Enable infinite loop
active: true, // Enable active slide indicator
direction: "ltr", // "ltr" or "rtl"
startIndex: 0, // Initial slide index
inViewThreshold: 0, // Visibility threshold
}Best Practices
1. Always include navigation - Previous/Next buttons or dots 2. Responsive sizing - Use max-w utilities for different screen sizes 3. Image optimization - Use React Image component 4. Accessibility - Supports keyboard navigation automatically 5. Touch support - Swipe gestures work on touch devices 6. Autoplay carefully - Consider user preference with stopOnInteraction 7. Performance - Use lazy loading for images
Accessibility
- Keyboard navigation: Arrow keys move between slides
- Focus management for navigation buttons
- Touch swipe gestures supported
- Respects
prefers-reduced-motion - ARIA labels on navigation controls
Chart Component
The Chart component wraps Recharts library with shadcn/ui styling. It provides ChartContainer, ChartTooltip, ChartLegend and other utilities for building responsive data visualizations.
Installation
Install the Chart component using the shadcn/ui CLI:
bunx --bun shadcn@latest add chartYou'll also need to install Recharts:
bun add rechartsComponents
ChartContainer
Main wrapper component that applies responsive sizing and theming.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
config | ChartConfig | - | Chart color configuration |
children | ReactNode | - | Recharts components |
className | string | - | Container CSS classes |
style | CSSProperties | - | Inline styles |
ChartTooltip
Displays formatted data on chart hover.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
content | ReactNode | - | Tooltip content component |
cursor | `boolean \ | object` | true |
ChartLegend
Displays legend for chart data series.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
content | ReactNode | - | Legend content component |
verticalAlign | `"top" \ | "bottom"` | "bottom" |
ChartTooltipContent & ChartLegendContent
Styled content components for tooltip and legend display.
Basic Bar Chart
Simple Bar Chart
Create a basic bar chart with grid:
"use client"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, type ChartConfig } from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "hsl(var(--chart-1))",
},
mobile: {
label: "Mobile",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig
export function BarChartExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickFormatter={(value) => value.slice(0, 3)} />
<Bar dataKey="desktop" fill="var(--color-desktop)" />
<Bar dataKey="mobile" fill="var(--color-mobile)" />
</BarChart>
</ChartContainer>
)
}Bar Chart with Tooltip
Add interactive tooltip to bar chart:
"use client"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "hsl(var(--chart-1))",
},
mobile: {
label: "Mobile",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig
export function BarChartWithTooltipExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickFormatter={(value) => value.slice(0, 3)} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" />
<Bar dataKey="mobile" fill="var(--color-mobile)" />
</BarChart>
</ChartContainer>
)
}Bar Chart with Legend
Add legend to bar chart:
"use client"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "hsl(var(--chart-1))",
},
mobile: {
label: "Mobile",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig
export function BarChartWithLegendExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickFormatter={(value) => value.slice(0, 3)} />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" />
<Bar dataKey="mobile" fill="var(--color-mobile)" />
</BarChart>
</ChartContainer>
)
}Line Chart
Basic Line Chart
Create a line chart for trend visualization:
"use client"
import { Line, LineChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ month: "January", value: 186 },
{ month: "February", value: 305 },
{ month: "March", value: 237 },
{ month: "April", value: 73 },
{ month: "May", value: 209 },
]
const chartConfig = {
value: {
label: "Value",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig
export function LineChartExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<LineChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Line
dataKey="value"
stroke="var(--color-value)"
dot={false}
isAnimationActive={true}
/>
</LineChart>
</ChartContainer>
)
}Area Chart
Basic Area Chart
Create an area chart:
"use client"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ month: "January", value: 186 },
{ month: "February", value: 305 },
{ month: "March", value: 237 },
{ month: "April", value: 73 },
{ month: "May", value: 209 },
]
const chartConfig = {
value: {
label: "Value",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig
export function AreaChartExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<AreaChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Area
dataKey="value"
fill="var(--color-value)"
stroke="var(--color-value)"
fillOpacity={0.4}
isAnimationActive={true}
/>
</AreaChart>
</ChartContainer>
)
}Pie Chart
Basic Pie Chart
Create a pie chart for distribution visualization:
"use client"
import { Pie, PieChart, Cell, Legend } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ name: "Desktop", value: 400 },
{ name: "Mobile", value: 300 },
{ name: "Tablet", value: 200 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "hsl(var(--chart-1))",
},
mobile: {
label: "Mobile",
color: "hsl(var(--chart-2))",
},
tablet: {
label: "Tablet",
color: "hsl(var(--chart-3))",
},
} satisfies ChartConfig
export function PieChartExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<PieChart>
<ChartTooltip content={<ChartTooltipContent />} />
<Pie
data={chartData}
dataKey="value"
cx="50%"
cy="50%"
outerRadius={100}
label
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={
index === 0
? "var(--color-desktop)"
: index === 1
? "var(--color-mobile)"
: "var(--color-tablet)"
}
/>
))}
</Pie>
<Legend />
</PieChart>
</ChartContainer>
)
}Responsive Charts
Mobile-Responsive Bar Chart
Create a responsive chart that adapts to screen size:
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/modules/cores/shadcn/components/ui/chart"
const chartData = [
{ month: "January", value: 186 },
{ month: "February", value: 305 },
{ month: "March", value: 237 },
{ month: "April", value: 73 },
{ month: "May", value: 209 },
]
const chartConfig = {
value: {
label: "Value",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig
export function ResponsiveBarChartExample() {
return (
<ChartContainer config={chartConfig} className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="value" fill="var(--color-value)" />
</BarChart>
</ResponsiveContainer>
</ChartContainer>
)
}Chart Configuration
ChartConfig Type
interface ChartConfig {
[key: string]: {
label: string
color?: string
icon?: ComponentType
}
}
// Example
const chartConfig = {
revenue: {
label: "Revenue",
color: "hsl(var(--chart-1))",
},
expenses: {
label: "Expenses",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfigBest Practices
1. Set min-height on ChartContainer - Required for responsive charts 2. Use theme colors - Reference CSS variables for consistency 3. Include tooltips - Always add ChartTooltip for better UX 4. Responsive sizing - Use percentage widths and fixed heights 5. Animate appropriately - Disable animations for large datasets 6. Format data labels - Use formatters for readable numbers 7. Legend positioning - Place legends where they don't obscure data
Recharts Documentation
For comprehensive Recharts documentation and advanced patterns, refer to Recharts official docs.
Styling
Custom Colors
const chartConfig = {
revenue: {
label: "Revenue",
color: "#3b82f6", // Tailwind blue-500
},
expenses: {
label: "Expenses",
color: "#ef4444", // Tailwind red-500
},
} satisfies ChartConfigTheme-Based Colors
// Light mode
const lightConfig = {
value: {
label: "Value",
color: "hsl(var(--chart-1))",
},
}
// Dark mode
const darkConfig = {
value: {
label: "Value",
color: "hsl(var(--chart-2))",
},
}Accessibility
- Proper ARIA labels on chart elements
- Keyboard navigation supported
- Color not the only visual indicator
- Responsive text sizing
- High contrast colors recommended
Field Component Patterns
Field component patterns for shadcn/ui with TanStack Form integration.
Imports
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from '@/modules/cores/shadcn/components/ui/field'---
Basic Field
<Field data-invalid={hasError}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" />
<FieldDescription>Your email address.</FieldDescription>
{hasError && <FieldError errors={errors} />}
</Field>---
Horizontal Field (Switches, Checkboxes)
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Notifications</FieldTitle>
<FieldDescription>Receive email notifications.</FieldDescription>
</FieldContent>
<Switch />
</Field>---
FieldGroup (Multiple Fields)
<FieldGroup>
<Field>
<FieldLabel htmlFor="firstName">First Name</FieldLabel>
<Input id="firstName" />
</Field>
<Field>
<FieldLabel htmlFor="lastName">Last Name</FieldLabel>
<Input id="lastName" />
</Field>
</FieldGroup>---
FieldSet with Legend
<FieldSet>
<FieldLegend>Personal Information</FieldLegend>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" />
</Field>
</FieldGroup>
</FieldSet>---
With TanStack Form
<form.Field
name="username"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input
id="username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
/>
<FieldDescription>3-10 characters.</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>