Documentation

Component Patterns

Preferred approaches for common UI scenarios.

Overview

These patterns represent the preferred way to build common UI in Catalyst. Following them ensures consistency and makes it easier for AI agents to understand and modify the codebase.

Forms

Simple Forms

When: POC stage, 1-5 fields, no complex validation

Approach:

  • Use native form with onSubmit handler
  • Use Field, FieldLabel, Input components
  • Basic client-side validation with required
  • Show errors inline below fields
<form onSubmit={handleSubmit}>
  <FieldGroup>
    <Field>
      <FieldLabel htmlFor="name">Name</FieldLabel>
      <Input id="name" required />
    </Field>
    <Button type="submit">Save</Button>
  </FieldGroup>
</form>

Complex Forms

When: MVP+ stage, 5+ fields, validation rules, multi-step

Approach:

  • Use react-hook-form for state management
  • Add zod for schema validation
  • Handle errors via form.formState.errors
  • Consider multi-step pattern for long forms
// Install: npm install react-hook-form zod @hookform/resolvers
const form = useForm<FormData>({
  resolver: zodResolver(schema),
})
// Then use form.register, form.handleSubmit, etc.

Tables

Basic Table

When: Static data, < 50 rows, no sorting/filtering

Approach:

  • Use Table, TableHeader, TableBody, TableRow, TableCell
  • Map data directly in JSX
  • No external library needed
<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Name</TableHead>
      <TableHead>Status</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {items.map((item) => (
      <TableRow key={item.id}>
        <TableCell>{item.name}</TableCell>
        <TableCell>{item.status}</TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

Advanced Table

When: Sorting, filtering, pagination, large datasets

Approach:

  • Use @tanstack/react-table for state
  • Wrap in components/vendors/data-table.tsx
  • Support column visibility, sorting, filtering
// Install: npm install @tanstack/react-table
// Create columns definition and use useReactTable hook
// See components/vendors for wrapper pattern

Empty States

Empty State Pattern

When: No data to display, first-time user, search with no results

Approach:

  • Centered layout with icon
  • Clear message explaining the state
  • Action button to resolve (add item, clear filters)
  • Keep it simple — don't over-illustrate
<div className="flex flex-col items-center py-12 text-center">
  <FileIcon className="h-12 w-12 text-muted-foreground" />
  <h3 className="mt-4 font-medium">No items yet</h3>
  <p className="text-muted-foreground mt-1 text-sm">
    Create your first item to get started.
  </p>
  <Button className="mt-4">Create Item</Button>
</div>

Loading States

Skeleton Loading

When: Content shape is known, perceived performance matters

Approach:

  • Use Skeleton component matching content layout
  • Show skeleton immediately while data loads
  • Don't over-skeleton — focus on primary content
// Loading state
<div className="space-y-4">
  <Skeleton className="h-8 w-1/3" />
  <Skeleton className="h-4 w-full" />
  <Skeleton className="h-4 w-2/3" />
</div>

// Loaded state
<div className="space-y-4">
  <h1>{data.title}</h1>
  <p>{data.description}</p>
</div>

Spinner Loading

When: Quick operations, buttons, unknown content shape

Approach:

  • Use spinner icon in buttons during submit
  • Disable button while loading
  • Show brief loading state, not skeletons
<Button disabled={isLoading}>
  {isLoading && <Spinner className="mr-2 h-4 w-4" />}
  {isLoading ? "Saving..." : "Save"}
</Button>

Dialogs

Confirmation Dialog

When: Destructive actions, irreversible operations

Approach:

  • Use AlertDialog for confirmations
  • Clear title stating the action
  • Explain consequences in description
  • Destructive action button styled appropriately
<AlertDialog>
  <AlertDialogTrigger asChild>
    <Button variant="destructive">Delete</Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete item?</AlertDialogTitle>
      <AlertDialogDescription>
        This cannot be undone.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Form Dialog

When: Quick create/edit without leaving context

Approach:

  • Use Dialog for forms
  • Keep form simple (< 5 fields)
  • Close on successful submit
  • Show loading state on submit button
<Dialog>
  <DialogTrigger asChild>
    <Button>Add Item</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Add Item</DialogTitle>
    </DialogHeader>
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <DialogFooter>
        <Button type="submit">Save</Button>
      </DialogFooter>
    </form>
  </DialogContent>
</Dialog>

Page Structure

App Page Pattern

When: Standard app page with header and content

Approach:

  • Page header with title and actions
  • Content area with appropriate max-width
  • Use consistent spacing (space-y-6 or space-y-8)
export default function Page() {
  return (
    <div className="space-y-6">
      {/* Page header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-semibold">Page Title</h1>
          <p className="text-muted-foreground">Description</p>
        </div>
        <Button>Action</Button>
      </div>

      {/* Content */}
      <div>{/* Main content here */}</div>
    </div>
  )
}

List/Detail Pattern

When: Master-detail views, item selection

Approach:

  • List view with selectable items
  • Detail view in panel or separate route
  • Maintain selection state
  • Handle empty selection state
// Option 1: Side panel (Sheet)
// Option 2: Separate route (/items/[id])
// Option 3: Split view (flex, list on left, detail on right)

Pattern Principle

Start simple. Use the basic pattern first. Only reach for the complex pattern when you have a clear need. Over-engineering in POC stage creates unnecessary work.

Related

See Examples for concrete implementations, and UI Components for the component library reference.