# Component Patterns - Spark App Template ## shadcn/ui Component Usage Guide This guide covers common patterns and best practices for using shadcn/ui components in Spark App Template applications. ## Core Principles 1. **Copy, Don't Import**: shadcn components are copied into your project, giving you full ownership 2. **Customize Freely**: Edit components in `src/components/ui/` to match your needs 3. **Compose, Don't Duplicate**: Build complex components by composing primitives 4. **Maintain Consistency**: Follow established patterns across your app ## Common Patterns ### Forms with Validation The canonical pattern for forms combines Form + react-hook-form + Zod: ```tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; const formSchema = z.object({ username: z.string().min(2, 'Username must be at least 2 characters'), email: z.string().email('Invalid email address'), }); export function ProfileForm() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { username: '', email: '', }, }); function onSubmit(values: z.infer) { console.log(values); } return (
( Username This is your public display name. )} /> ( Email )} /> ); } ``` ### Dialogs & Modals Standard pattern for modal dialogs: ```tsx import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; export function DeleteConfirmDialog() { return ( Are you sure? This action cannot be undone. This will permanently delete your account and remove your data from our servers. ); } ``` ### Cards with Actions Pattern for content cards with interactive elements: ```tsx import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; interface ProjectCardProps { title: string; description: string; status: 'active' | 'archived'; onEdit: () => void; onDelete: () => void; } export function ProjectCard({ title, description, status, onEdit, onDelete, }: ProjectCardProps) { return (
{title} {status}
{description}
); } ``` ### Data Tables Pattern for displaying tabular data: ```tsx import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { MoreHorizontal } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; interface User { id: string; name: string; email: string; role: string; status: 'active' | 'inactive'; } export function UserTable({ users }: { users: User[] }) { return ( Name Email Role Status {users.map((user) => ( {user.name} {user.email} {user.role} {user.status} Edit Delete ))}
); } ``` ### Select Dropdowns Pattern for select/dropdown inputs: ```tsx import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; export function StatusSelect() { return ( ); } ``` With react-hook-form: ```tsx ( Status )} /> ``` ### Toast Notifications Using Sonner for toast notifications: ```tsx import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; export function ToastExample() { return (
); } ``` ### Tabs for Content Organization ```tsx import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Card, CardContent } from '@/components/ui/card'; export function SettingsTabs() { return ( General Security Notifications General settings content Security settings content ); } ``` ### Accordion for FAQ / Expandable Content ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; export function FAQ() { return ( How do I get started? Getting started is easy! Simply sign up for an account and follow our onboarding guide. What payment methods do you accept? We accept all major credit cards, PayPal, and bank transfers. ); } ``` ### Command Palette (⌘K) ```tsx import { useEffect, useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@/components/ui/command'; export function CommandPalette() { const [open, setOpen] = useState(false); const navigate = useNavigate(); useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((open) => !open); } }; document.addEventListener('keydown', down); return () => document.removeEventListener('keydown', down); }, []); return ( No results found. navigate({ to: '/dashboard' })}> Dashboard navigate({ to: '/settings' })}> Settings Create New Project Invite Team Member ); } ``` ### Sheet (Drawer) for Mobile Navigation ```tsx import { Menu } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet'; export function MobileNav() { return ( Navigation Navigate to different sections of the app ); } ``` ### Popovers for Contextual Content ```tsx import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Calendar } from '@/components/ui/calendar'; export function DatePickerPopover() { const [date, setDate] = useState(); return ( ); } ``` ### Alert / Alert Dialog for Important Messages **Alert** (non-blocking): ```tsx import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { InfoIcon } from 'lucide-react'; export function InfoAlert() { return ( Heads up! You can add components to your app using the CLI. ); } ``` **AlertDialog** (blocking confirmation): ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; export function DeleteAlert() { return ( Are you absolutely sure? This action cannot be undone. This will permanently delete your account and remove your data from our servers. Cancel Continue ); } ``` ## Composition Patterns ### Combining Components Build complex UIs by composing primitives: ```tsx // Filter panel combining multiple components export function FilterPanel() { return ( Filters
); } ``` ## Card Layout Patterns ### Understanding Card Spacing shadcn Card components come with default spacing optimized for content cards but may need adjustment when used as structural containers. **Default Card structure:** - `CardHeader`: Uses `p-6` padding (1.5rem/24px on all sides) - `CardContent`: Uses `p-6 pt-0` padding (removes top padding to prevent double spacing with CardHeader) - Card wrapper: May include `py-6` and `gap-6` in some versions **Key principle:** "Keep card content focused - cards work best when covering one topic" ([source](https://medium.com/@rivainasution/shadcn-ui-react-series-part-6-card-the-container-i-use-everywhere-without-thinking-a6f9dd711d18)) ### When to Override Card Defaults shadcn explicitly supports overriding defaults via className props ([Card docs](https://ui.shadcn.com/docs/components/card)). Here's when to use each pattern: **Content Cards (Keep defaults):** Use default spacing when Card represents discrete content: ```tsx // ✅ Good - Content card benefits from default spacing Project Updates Latest changes to your project

Each card should cover one focused topic

``` **Layout Cards (Override with p-0):** Remove spacing when Card serves as a structural container: ```tsx // ✅ Good - Layout card with full control over spacing

Panel Header

{/* Panel content with independent spacing */}
``` **Partial Overrides:** When you need asymmetric spacing control: ```tsx // ✅ Good - Remove top padding only, preserve bottom {/* Controlled top spacing, natural bottom spacing */} ``` ### Full-Height Card Layouts For Cards that need to fill available vertical space: ```tsx // Pattern: flex-col on Card, flex-1 on content area

Fixed Header

{/* Scrollable content fills remaining space */}
``` **Why this works:** Setting `flex: 1` on the content area allows it to "grow and take all available space" while pushing footers to the bottom ([CSS-Tricks](https://css-tricks.com/boxes-fill-height-dont-squish/)). ### Cards with ScrollArea ScrollArea requires careful padding coordination: ```tsx // ✅ Correct pattern - padding inside ScrollArea Scrollable Content {/* Padding applied here, not on CardContent */}
{items.map(item =>
{item.content}
)}
``` **Key points:** - Always set explicit height on ScrollArea (e.g., `h-[400px]`) - Place padding inside ScrollArea, not on parent CardContent - This prevents double scrollbars and layout issues ([Medium - ScrollArea](https://medium.com/@rivainasution/shadcn-ui-react-series-part-9-scroll-area-controlled-scrolling-without-layout-hacks-4263c6f899f4)) ### Overflow-Hidden Considerations The `overflow-hidden` class is commonly needed for layout cards but intentionally not default: ```tsx // When to use overflow-hidden: {/* Clips content to rounded corners */} {/* Image respects border-radius */} ... ``` There's ongoing discussion about making this default, but it remains optional to support both `overflow: hidden` and `overflow: scroll` use cases ([GitHub #2885](https://github.com/shadcn-ui/ui/issues/2885)). ### Split Panel Pattern Common pattern for side-by-side cards in dashboard layouts: ```tsx

Left Panel

{/* Scrollable left content */}

Right Panel

{/* Scrollable right content */}
``` ### Nested Cards Pattern Outer layout card with inner content cards: ```tsx {/* Outer: Layout card with no default spacing */}

Main Panel

{/* Inner: Content cards keep default spacing */} Section A Content benefits from Card's natural padding Section B Another content card with default spacing
``` ### Card Spacing Decision Tree Ask yourself these questions when using Cards: 1. **Is this a content card or layout card?** - Content: Represents discrete information → Keep defaults - Layout: Structural container → Override with `p-0 gap-0` 2. **Does the Card need to fill available height?** - Yes → Use `flex flex-col` on Card, `flex-1` on content area - Add `overflow-auto` on content area if scrolling needed 3. **Are there nested Cards?** - Outer Card: Override spacing (`p-0 gap-0`) for layout control - Inner Cards: Keep defaults for proper content spacing 4. **Using ScrollArea inside Card?** - CardContent needs `p-0` - Move padding into ScrollArea - Set explicit height on ScrollArea 5. **Need clipped corners for images?** - Add `overflow-hidden` to Card wrapper ## Best Practices ### 1. Use Variants ```tsx ``` ### 2. Consistent Sizing ```tsx ``` ### 3. Accessible Labels ```tsx ``` ### 4. Loading States ```tsx ``` ### 5. Empty States ```tsx {items.length === 0 ? (

No projects found

Get started by creating a new project.

) : ( )} ``` ## Common Mistakes to Avoid ❌ **Don't** nest interactive elements: ```tsx ``` ✅ **Do** use asChild: ```tsx ``` ❌ **Don't** ignore form validation feedback: ```tsx {/* Missing FormMessage */} ``` ✅ **Do** show validation errors: ```tsx {/* Shows errors */} ``` ❌ **Don't** use inline styles for theming: ```tsx