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:
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:
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>
Sidebar Layout¶
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:
Conditional Display:
Responsive Spacing:
Accessibility Patterns¶
Semantic HTML¶
Use semantic elements:
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:
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:
Secondary Button:
Input Field:
Table Header:
Table Row:
Table Cell:
Future Enhancements¶
- Design Tokens: Centralized design system with CSS variables
- Dark Mode: Support for dark theme
- Component Library: Storybook documentation
- Animation Library: Framer Motion integration
- Advanced Tables: Sorting, filtering, pagination components
- Date/Time Pickers: Specialized input components
- Rich Text Editor: For descriptions and notes
- File Upload: Drag-and-drop file components
- Data Visualization: Chart components (Chart.js, Recharts)
- Skeleton Loaders: Better loading states
Resources¶
- Radix UI Documentation
- Tailwind CSS Documentation
- Lucide Icons
- shadcn/ui - Component inspiration
- Tailwind UI - Design patterns