Skip to content

Component Patterns & UI Guidelines

Overview

This guide documents the component patterns, UI conventions, and design system used in the LCO DMA frontend application. The application uses a combination of Radix UI primitives and custom-styled components with Tailwind CSS.

Component Categories

1. UI Primitives (components/ui/)

Reusable, unstyled or minimally-styled components based on Radix UI.

Available Components

Component Source Purpose File
Button Radix Slot Clickable actions with variants ui/button.tsx
Input Native HTML Text input fields ui/input.tsx
Textarea Native HTML Multi-line text input ui/textarea.tsx
Label Radix Label Form field labels ui/label.tsx
Select Radix Select Dropdown selection ui/select.tsx
Dialog Radix Dialog Modal overlays ui/dialog.tsx
Alert Dialog Radix Alert Dialog Confirmation dialogs ui/alert-dialog.tsx
Dropdown Menu Radix Dropdown Contextual menus ui/dropdown-menu.tsx
Popover Radix Popover Floating content ui/popover.tsx
Table Native HTML Data tables ui/table.tsx
Tabs Radix Tabs Tabbed navigation ui/tabs.tsx
Toast Radix Toast Notifications ui/toast.tsx + ui/toaster.tsx
Combobox CMDK + Radix Popover Searchable dropdown ui/combobox.tsx
Command CMDK Command palette ui/command.tsx
Accordion Radix Accordion Collapsible sections ui/accordion.tsx
Avatar Radix Avatar User avatars ui/avatar.tsx
Card Native HTML Content containers ui/card.tsx
Switch Radix Switch Toggle switches ui/switch.tsx
Breadcrumb Custom Navigation breadcrumbs ui/breadcrumb.tsx

2. Advanced Table Components (components/table/)

Built on @tanstack/react-table for complex data management.

Component Purpose File
VirtualizedDataTable Performance table with virtualization + infinite scroll table/VirtualizedDataTable.tsx (798 lines)
DataTable Standard table with filtering, sorting, search table/DataTable.tsx
EditableCell Inline cell editing with validation table/EditableCell.tsx
BulkEditPopover Bulk edit multiple selected rows table/BulkEditPopover.tsx
ColumnVisibilityDialog Show/hide columns with drag-and-drop table/ColumnVisibilityDialog.tsx
DataTableFilterBuilder Complex filter builder (AND/OR logic) table/DataTableFilterBuilder.tsx
DataTableSortMenu Multi-column sorting menu table/DataTableSortMenu.tsx
DataTableToolbar Table toolbar with actions table/DataTableToolbar.tsx
FloatingActionBar Floating action bar for selected rows table/FloatingActionBar.tsx
SelectionBanner Page/all selection banner table/SelectionBanner.tsx
TableDataContext Context for sharing table data table/TableDataContext.tsx

Virtualization Components (components/virtualized/)

Component Purpose File
VirtualizedTableBody Reusable virtualized tbody virtualized/VirtualizedTableBody.tsx (267 lines)

Key Features: - Virtualization: Only renders visible rows plus configurable overscan - Infinite scroll: Automatic loadMore triggering near list end - Inline cell editing with type validation (number, text, select) - Bulk edit and delete operations with selection context - Advanced filtering with AND/OR groups - Column visibility management with drag-and-drop reordering - Row selection and bulk actions (page or all items) - Search across all visible columns - Sort by multiple columns - Context-based data sharing - Table state persistence: Filters, sorting, column visibility saved to localStorage

3. Feature Components (components/)

Business-specific components with application logic.

Component Purpose Key Features
PageHeader Page titles with breadcrumbs Consistent header layout across all pages
AppSidebar Main navigation sidebar shadcn Sidebar component with project context
COATable Chart of Accounts tree (686 lines) Hierarchical tree structure with expand/collapse
CrewsTable Crew selection and management in service Combobox for crew selection, rate calculation
DisciplineCard Discipline navigation card Links to discipline sheets in MTO
WBSTable Work Breakdown Structure table Hierarchical task breakdown display
MaterialTable Material catalog table Material items management
ServiceDialog Service creation/editing Modal with service type selection
ConfirmationDialog Delete/action confirmations Reusable alert dialog wrapper
CreateCrewDialog Crew creation workflow Multi-step crew composition builder
CreateCrewMemberDialog Crew member creation Location and rate card inputs
CreateCrewTradeDialog Crew trade creation Trade definition form
CreateEquipmentDialog Equipment creation Equipment catalog management
CreateMaterialDialog Material creation Material catalog management
AddEquipmentTagDialog Equipment tag creation Equipment tag management
IngestMTODialog MTO file import Upload and parse MTO Excel files
ImportCrewsDialog Bulk crew import Import multiple crews to service
ImportEquipmentTagsDialog Bulk equipment tag import Import tags from Excel
ImportWBSDialog Bulk WBS import Import WBS from Excel
ProjectInformationAccordion Project metadata display Collapsible project details
ProjectWBSView Project WBS hierarchy view Display WBS tree structure

Estimation Tab Components (components/estimation-tabs/)

Component Purpose
EstimationMasterTab Aggregated estimation view across disciplines
MTODisciplineTab MTO data by discipline with VirtualizedDataTable
CrewsTab Service crew management and rate calculations
EquipmentTagsTab Equipment tag CRUD with infinite scroll
SubcontractorsTab Subcontractor management
MaterialCostTab Material cost entries
LaborSettingsTab Labor configuration and indirect costs
LoadingSpinner Shared loading indicator

4. Page Components (pages/)

Full-page layouts that compose features and handle routing.

Design System

Color Palette

Neutral Colors (Primary UI) - From Tailwind Config

neutral-50: #fafafa   /* Light backgrounds */
neutral-100: #f5f5f5  /* Secondary backgrounds (main content area) */
neutral-200: #e5e5e5  /* Borders, dividers */
neutral-500: #737373  /* Disabled/muted text */
neutral-700: #404040  /* Icons */
neutral-950: #0a0a0a  /* Primary text, headings */

Brand Colors - From Tailwind Config

blue-900: #1e3a8a    /* Primary action color (buttons, active states) */
blue-800: #1e40af    /* Button hover states */
blue-600: #2563eb    /* Links, accents */

Semantic Colors

red-600: #dc2626     /* Destructive actions, errors */
green-600: #16a34a   /* Success states */
yellow-500: #eab308  /* Warnings */

Usage Guidelines: - Use neutral-950 for primary text - Use neutral-500 for muted/secondary text - Use blue-900 for primary action buttons - Use neutral-100 for page backgrounds - Use neutral-200 for borders throughout

Typography

Font Sizes

text-xs: 0.75rem     /* 12px - Small labels */
text-sm: 0.875rem    /* 14px - Body text, table cells */
text-base: 1rem      /* 16px - Default */
text-lg: 1.125rem    /* 18px - Headings, page titles */
text-xl: 1.25rem     /* 20px - Large headings */

Font Weights

font-normal: 400     /* Body text */
font-medium: 500     /* Table headers, labels */
font-semibold: 600   /* Sidebar branding, emphasis */

Spacing

Standard spacing scale (Tailwind defaults):

p-2: 0.5rem    /* 8px */
p-4: 1rem      /* 16px */
p-6: 1.5rem    /* 24px - Common page padding */
gap-2: 0.5rem  /* 8px */
gap-4: 1rem    /* 16px */
gap-6: 1.5rem  /* 24px */

Shadows

shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05)
shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)

Custom shadow for cards:

shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_-1px_rgba(0,0,0,0.1)]

Component Patterns

Button Component

Usage:

import { Button } from '@/components/ui/button';

<Button variant="default">Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>

Variants:

Variant Style Use Case
default Blue-900 background Primary actions (Save, Create)
outline Border only Secondary actions (Cancel)
ghost No background Icon buttons, minimal actions
destructive Red background Delete, remove actions

Sizes:

<Button size="sm">Small</Button>    {/* h-8 */}
<Button size="default">Default</Button>  {/* h-9 */}
<Button size="lg">Large</Button>    {/* h-10 */}
<Button size="icon">Icon</Button>   {/* h-9 w-9 */}

Common Patterns:

Loading state:

<Button disabled={isLoading}>
  {isLoading ? (
    <>
      <Loader2 className="h-4 w-4 mr-2 animate-spin" />
      Saving...
    </>
  ) : (
    'Save'
  )}
</Button>

With icon:

<Button>
  <Plus className="h-4 w-4 mr-2" />
  Add Item
</Button>

Table Component

Standard Pattern:

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

<Table>
  <TableHeader>
    <TableRow className="border-b border-neutral-200">
      <TableHead className="text-neutral-500 font-medium h-10">
        Column Name
      </TableHead>
      {/* More headers */}
    </TableRow>
  </TableHeader>
  <TableBody>
    {items.map((item) => (
      <TableRow key={item.id} className="border-b border-neutral-200 h-[52px]">
        <TableCell className="font-normal text-neutral-950">
          {item.name}
        </TableCell>
        {/* More cells */}
      </TableRow>
    ))}
  </TableBody>
</Table>

Standard Dimensions: - Header row: h-10 - Body row: h-[52px] - Border: border-b border-neutral-200

Empty State:

<TableBody>
  {items.length === 0 ? (
    <TableRow>
      <TableCell colSpan={6} className="text-center py-8 text-neutral-500">
        No items found
      </TableCell>
    </TableRow>
  ) : (
    // Normal rows
  )}
</TableBody>

Action Column (Dropdown):

<TableCell className="text-center">
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="ghost" className="h-8 w-8 p-0 hover:bg-neutral-100">
        <MoreVertical className="h-4 w-4" />
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">
      <DropdownMenuItem onClick={handleEdit}>
        Edit
      </DropdownMenuItem>
      <DropdownMenuItem onClick={handleDelete} className="text-red-600">
        Delete
      </DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</TableCell>

Advanced Data Table (TanStack Table)

Complete Example with All Features:

import { DataTable } from '@/components/table/DataTable';
import { ColumnDef } from '@tanstack/react-table';
import { FilterGroup } from '@/components/table/types';
import { SortingState } from '@tanstack/react-table';

interface MyData {
  id: string;
  name: string;
  quantity: number;
  status: string;
}

const columns: ColumnDef<MyData>[] = [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => <span>{row.original.name}</span>,
  },
  {
    accessorKey: 'quantity',
    header: 'Quantity',
    cell: ({ row }) => (
      <EditableCell
        value={row.original.quantity}
        type="number"
        onSave={(newValue) => handleCellEdit(row.id, { quantity: newValue })}
      />
    ),
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => <span>{row.original.status}</span>,
  },
];

function MyTablePage() {
  const [data, setData] = useState<MyData[]>([]);
  const [filters, setFilters] = useState<FilterGroup>({
    id: 'root',
    logicalOperator: 'AND',
    conditions: [],
  });
  const [sorting, setSorting] = useState<SortingState>([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedRows, setSelectedRows] = useState<MyData[]>([]);

  const handleBulkEdit = async (rows: MyData[], updates: Record<string, unknown>) => {
    // Bulk edit logic
    console.log('Editing rows:', rows, 'with updates:', updates);
  };

  const handleBulkDelete = async (rows: MyData[]) => {
    // Bulk delete logic
    console.log('Deleting rows:', rows);
  };

  const handleCellEdit = (rowId: string, updates: Record<string, unknown>) => {
    // Update single cell
    setData(prev => prev.map(item =>
      item.id === rowId ? { ...item, ...updates } : item
    ));
  };

  return (
    <DataTable
      data={data}
      columns={columns}
      // Filtering
      filters={filters}
      onFiltersChange={setFilters}
      // Sorting
      sorting={sorting}
      onSortingChange={setSorting}
      // Search
      searchTerm={searchTerm}
      onSearchChange={setSearchTerm}
      // Row selection
      enableRowSelection={true}
      selectedRows={selectedRows}
      onSelectedRowsChange={setSelectedRows}
      // Column visibility
      enableColumnVisibility={true}
      // Bulk operations
      enableBulkEdit={true}
      onBulkEdit={handleBulkEdit}
      enableBulkDelete={true}
      onBulkDelete={handleBulkDelete}
      // UI customization
      emptyMessage="No items found"
      loading={false}
    />
  );
}

Editable Cell with Validation:

import { EditableCell } from '@/components/table/EditableCell';

// Number input with validation
<EditableCell
  value={row.original.quantity}
  type="number"
  min={0}
  max={1000}
  onSave={(newValue) => handleCellEdit(row.id, { quantity: newValue })}
/>

// Text input
<EditableCell
  value={row.original.name}
  type="text"
  maxLength={100}
  onSave={(newValue) => handleCellEdit(row.id, { name: newValue })}
/>

// Select input
<EditableCell
  value={row.original.status}
  type="select"
  options={[
    { value: 'active', label: 'Active' },
    { value: 'inactive', label: 'Inactive' },
  ]}
  onSave={(newValue) => handleCellEdit(row.id, { status: newValue })}
/>

Advanced Filter Structure:

// Simple filter
const simpleFilter: FilterGroup = {
  id: 'root',
  logicalOperator: 'AND',
  conditions: [
    { field: 'name', operator: 'contains', value: 'test' },
    { field: 'quantity', operator: 'greaterThan', value: 10 },
  ],
};

// Complex nested filter (name contains "test" OR quantity > 10)
const complexFilter: FilterGroup = {
  id: 'root',
  logicalOperator: 'OR',
  conditions: [
    { field: 'name', operator: 'contains', value: 'test' },
    { field: 'quantity', operator: 'greaterThan', value: 10 },
  ],
};

// Available operators
type FilterOperator =
  | 'equals'
  | 'notEquals'
  | 'contains'
  | 'notContains'
  | 'greaterThan'
  | 'greaterThanOrEqual'
  | 'lessThan'
  | 'lessThanOrEqual';

Dialog/Modal Component

Basic Pattern:

import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';

const [open, setOpen] = useState(false);

<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent className="sm:max-w-[500px]">
    <DialogHeader>
      <DialogTitle>Dialog Title</DialogTitle>
    </DialogHeader>
    <div className="space-y-4">
      {/* Form content */}
    </div>
    <div className="flex justify-end gap-2">
      <Button variant="outline" onClick={() => setOpen(false)}>
        Cancel
      </Button>
      <Button onClick={handleSave}>Save</Button>
    </div>
  </DialogContent>
</Dialog>

Dialog Sizes:

sm:max-w-[425px]  /* Small - Simple forms */
sm:max-w-[500px]  /* Medium - Standard */
sm:max-w-[700px]  /* Large - Complex forms */
sm:max-w-[900px]  /* Extra large - Multi-section */

Form Pattern

Standard Form Layout:

import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';

<form onSubmit={handleSubmit} className="space-y-4">
  <div className="space-y-2">
    <Label htmlFor="field1">Field Label *</Label>
    <Input
      id="field1"
      value={formData.field1}
      onChange={(e) => setFormData({...formData, field1: e.target.value})}
      placeholder="Enter value"
      required
    />
  </div>

  <div className="space-y-2">
    <Label htmlFor="field2">Select Field</Label>
    <Select value={formData.field2} onValueChange={(value) => setFormData({...formData, field2: value})}>
      <SelectTrigger id="field2">
        <SelectValue placeholder="Select option" />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="option1">Option 1</SelectItem>
        <SelectItem value="option2">Option 2</SelectItem>
      </SelectContent>
    </Select>
  </div>

  <div className="flex justify-end gap-2">
    <Button type="button" variant="outline">Cancel</Button>
    <Button type="submit">Save</Button>
  </div>
</form>

Form Validation Pattern:

const [errors, setErrors] = useState<Record<string, string>>({});

const validate = () => {
  const newErrors: Record<string, string> = {};

  if (!formData.name) {
    newErrors.name = 'Name is required';
  }

  setErrors(newErrors);
  return Object.keys(newErrors).length === 0;
};

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  if (!validate()) return;

  // Submit logic
};

// Display error
{errors.name && (
  <p className="text-sm text-red-600">{errors.name}</p>
)}

Tabs Pattern

Standard Implementation:

import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

const [activeTab, setActiveTab] = useState('tab1');

<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
  <TabsList className="h-auto w-full justify-start rounded-none border-b border-neutral-200 bg-transparent p-0 px-6">
    <TabsTrigger
      value="tab1"
      className="rounded-none border-b-2 border-transparent px-[10px] py-2 shadow-none data-[state=active]:border-blue-900 data-[state=active]:bg-transparent data-[state=active]:text-neutral-950 data-[state=inactive]:text-neutral-500"
    >
      Tab 1
    </TabsTrigger>
    <TabsTrigger value="tab2" className="...">
      Tab 2
    </TabsTrigger>
  </TabsList>

  <TabsContent value="tab1" className="flex-1 bg-neutral-100 p-6 mt-0">
    {/* Tab 1 content */}
  </TabsContent>

  <TabsContent value="tab2" className="flex-1 bg-neutral-100 p-6 mt-0">
    {/* Tab 2 content */}
  </TabsContent>
</Tabs>

Styling Constants: - Active border: border-blue-900 - Active text: text-neutral-950 - Inactive text: text-neutral-500 - Padding: px-[10px] py-2

Combobox (Searchable Dropdown)

Pattern:

import { Combobox } from '@/components/ui/combobox';

const options = [
  { value: '1', label: 'Option 1' },
  { value: '2', label: 'Option 2' },
];

<Combobox
  options={options}
  value={selectedValue}
  onValueChange={setSelectedValue}
  placeholder="Select option"
  searchPlaceholder="Search..."
  emptyText="No results found."
  className="h-9"
/>

Use Cases: - Crew selection (CrewsTable.tsx:126-135) - Large dropdown lists (100+ items) - When search functionality is needed

PageHeader Pattern

Standard Usage:

import { PageHeader } from '@/components/PageHeader';

const breadcrumbs = [
  { label: 'Home', href: '/' },
  { label: 'Projects', href: '/projects' },
  { label: 'Current Project' },
];

<PageHeader title="Page Title" breadcrumbs={breadcrumbs} />

Styling: - Height: h-[60px] - Background: bg-white - Border: border-b border-neutral-200

Accordion Pattern

Collapsible Sections:

import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';

<Accordion type="single" collapsible defaultValue="item-1">
  <AccordionItem value="item-1" className="border-b border-neutral-200">
    <AccordionTrigger className="text-neutral-950 font-medium">
      Section Title
    </AccordionTrigger>
    <AccordionContent>
      {/* Content */}
    </AccordionContent>
  </AccordionItem>
</Accordion>

Example: ProjectInformationAccordion (components/ProjectInformationAccordion.tsx)

Toast Notifications

Pattern:

import { useToast } from '@/hooks/use-toast';

const { toast } = useToast();

// Success
toast({
  title: 'Success',
  description: 'Operation completed successfully',
});

// Error
toast({
  title: 'Error',
  description: 'Something went wrong',
  variant: 'destructive',
});

// Info
toast({
  title: 'Info',
  description: 'Information message',
});

Placement: Bottom-right corner (configured in Toaster component)

Loading States

Spinner:

import { Loader2 } from 'lucide-react';

// Full page
<div className="flex h-full items-center justify-center">
  <Loader2 className="h-8 w-8 animate-spin text-blue-600" />
</div>

// Inline
<Loader2 className="h-4 w-4 animate-spin" />

Button Loading:

<Button disabled={isLoading}>
  {isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
  {isLoading ? 'Loading...' : 'Submit'}
</Button>

Table Cell Loading:

<TableCell className="p-2">
  <div className="h-9 flex items-center px-3">
    {isCalculating ? (
      <Loader2 className="h-4 w-4 animate-spin text-neutral-400" />
    ) : (
      value
    )}
  </div>
</TableCell>

Layout Patterns

Page Layout

Standard Full-Height Page:

export function MyPage() {
  return (
    <div className="flex flex-col h-full">
      <PageHeader title="Title" breadcrumbs={breadcrumbs} />

      <div className="flex-1 bg-neutral-100 p-6">
        <div className="bg-white p-6 shadow-sm">
          {/* Content */}
        </div>
      </div>
    </div>
  );
}

With Tabs:

<div className="flex flex-col h-full">
  <PageHeader title="Title" breadcrumbs={breadcrumbs} />

  <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
    <TabsList className="...">
      {/* Tabs */}
    </TabsList>

    <TabsContent value="tab1" className="flex-1 bg-neutral-100 p-6 mt-0">
      {/* Content */}
    </TabsContent>
  </Tabs>
</div>

Card Layout

Content Card:

<div className="bg-white border border-neutral-200 p-6 shadow-sm">
  <h3 className="text-lg font-medium text-neutral-950 mb-4">
    Section Title
  </h3>
  {/* Content */}
</div>

Clickable Card:

<div
  className="bg-white border border-neutral-200 p-6 shadow-sm cursor-pointer hover:border-blue-900 transition-colors"
  onClick={handleClick}
>
  {/* Content */}
</div>

Main App Layout:

<div className="flex h-screen">
  <Sidebar />
  <main className="flex-1 overflow-auto bg-gray-50">
    <Routes>
      {/* Routes */}
    </Routes>
  </main>
  <Toaster />
</div>

Sidebar Width: - Expanded: w-[20vw] - Collapsed: w-16

Responsive Design

Breakpoints (Tailwind defaults)

sm: 640px   /* Small devices */
md: 768px   /* Medium devices */
lg: 1024px  /* Large devices */
xl: 1280px  /* Extra large devices */
2xl: 1536px /* 2X large devices */

Common Responsive Patterns

Grid Layout:

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {/* Items */}
</div>

Conditional Display:

<div className="hidden md:block">Desktop only</div>
<div className="md:hidden">Mobile only</div>

Responsive Spacing:

<div className="p-4 md:p-6 lg:p-8">
  {/* Padding increases with screen size */}
</div>

Accessibility Patterns

Semantic HTML

Use semantic elements:

<main>, <nav>, <header>, <section>, <article>

ARIA Labels

<button aria-label="Close dialog" onClick={handleClose}>
  <X className="h-4 w-4" />
</button>

<input aria-describedby="email-error" />
<p id="email-error" className="text-red-600">Invalid email</p>

Keyboard Navigation

Ensure all interactive elements are keyboard accessible:

<div
  role="button"
  tabIndex={0}
  onKeyDown={(e) => e.key === 'Enter' && handleClick()}
  onClick={handleClick}
>
  Click me
</div>

Focus States

All interactive elements have visible focus:

<button className="focus:outline-none focus:ring-2 focus:ring-blue-900">
  Click
</button>

Icon Usage (Lucide React)

Common Icons

import {
  Home,
  FolderOpen,
  Table,
  Plus,
  MoreVertical,
  ChevronDown,
  ArrowLeft,
  Loader2,
  Pencil,
  Trash,
  X,
} from 'lucide-react';

// Standard size
<Home className="h-4 w-4" />

// Larger
<Home className="h-6 w-6" />

// With color
<Home className="h-4 w-4 text-blue-900" />

// Spinning (loading)
<Loader2 className="h-4 w-4 animate-spin" />

Icon Sizing

  • Small icons (buttons, inline): h-4 w-4 (16px)
  • Medium icons: h-5 w-5 (20px)
  • Large icons (page spinners): h-8 w-8 (32px)

Animation Patterns

Transitions

// Hover transition
className="transition-colors hover:bg-gray-100"

// All properties
className="transition-all duration-300"

// Custom
className="transition-opacity duration-200 ease-in-out"

Tailwind Animate Plugin

// Spin (for loaders)
className="animate-spin"

// Accordion (built-in to Radix)
className="animate-accordion-down"
className="animate-accordion-up"

Best Practices

1. Component Composition

Good:

<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Title</DialogTitle>
    </DialogHeader>
    <div>{/* Content */}</div>
  </DialogContent>
</Dialog>

Avoid:

// Don't recreate Radix functionality
<div className="dialog-overlay" onClick={closeDialog}>
  <div className="dialog-content">...</div>
</div>

2. Consistent Spacing

Use consistent spacing scale:

// Good
<div className="space-y-4">  {/* Consistent 1rem gap */}

// Avoid mixing
<div className="space-y-3">  {/* 0.75rem */}
<div className="space-y-5">  {/* 1.25rem */}

3. Color Usage

Stick to neutral palette for UI, brand colors for actions:

// Good
<p className="text-neutral-950">Text</p>
<Button className="bg-blue-900">Action</Button>

// Avoid custom colors
<p className="text-[#333]">Text</p>

4. Conditional Styling

Use cn() utility for clean conditional classes:

import { cn } from '@/lib/utils';

// Good
<div className={cn(
  'base-class',
  isActive && 'active-class',
  variant === 'primary' && 'primary-class'
)} />

// Avoid string concatenation
<div className={'base-class' + (isActive ? ' active-class' : '')} />

5. Component Reusability

Extract repeated patterns:

// Bad - Repeated dropdown pattern
<DropdownMenu>...</DropdownMenu>
<DropdownMenu>...</DropdownMenu>

// Good - Create ActionsDropdown component
<ActionsDropdown onEdit={handleEdit} onDelete={handleDelete} />

6. Accessibility First

Always include: - Semantic HTML - ARIA labels where needed - Keyboard navigation - Focus states - Color contrast (WCAG AA minimum)

Common Anti-Patterns to Avoid

1. Inline Styles

// Bad
<div style={{ color: 'red', fontSize: 14 }}>Text</div>

// Good
<div className="text-red-600 text-sm">Text</div>

2. Magic Numbers

// Bad
<div className="h-[37px] w-[283px]">

// Good - Use standard spacing
<div className="h-9 w-full">

3. Over-nesting

// Bad
<div>
  <div>
    <div>
      <div>Content</div>
    </div>
  </div>
</div>

// Good - Flatten when possible
<div className="outer">
  <div className="inner">Content</div>
</div>

4. Hardcoded Values

// Bad
{items.slice(0, 10).map(...)}

// Good
const PAGE_SIZE = 10;
{items.slice(0, PAGE_SIZE).map(...)}

Quick Reference

Standard Heights

h-8: 2rem      /* 32px - Small buttons */
h-9: 2.25rem   /* 36px - Default buttons/inputs */
h-10: 2.5rem   /* 40px - Table headers */
h-[52px]: 52px /* Table rows */
h-[60px]: 60px /* Page header */

Standard Widths

w-full: 100%
w-[20vw]: 20% of viewport /* Sidebar */
sm:max-w-[500px]: 500px max on small+ screens /* Dialogs */

Common Combinations

Primary Button:

className="h-9 px-4 bg-blue-900 hover:bg-blue-800 text-white"

Secondary Button:

className="h-9 px-4 border-neutral-200 shadow-sm hover:bg-neutral-100"

Input Field:

className="h-9 w-full border-neutral-200 focus:ring-blue-900"

Table Header:

className="text-neutral-500 font-medium h-10"

Table Row:

className="border-b border-neutral-200 h-[52px]"

Table Cell:

className="font-normal text-neutral-950 p-2"

Future Enhancements

  1. Design Tokens: Centralized design system with CSS variables
  2. Dark Mode: Support for dark theme
  3. Component Library: Storybook documentation
  4. Animation Library: Framer Motion integration
  5. Advanced Tables: Sorting, filtering, pagination components
  6. Date/Time Pickers: Specialized input components
  7. Rich Text Editor: For descriptions and notes
  8. File Upload: Drag-and-drop file components
  9. Data Visualization: Chart components (Chart.js, Recharts)
  10. Skeleton Loaders: Better loading states

Resources