Component Patterns¶
Conventions for building consistent UI components.
Layout Components¶
PageShell¶
Standard page wrapper with consistent layout.
<PageShell maxWidth="md" scrollable>
<PageHeader title={t("markers")} />
<SectionCard>
<MarkerList markers={markers} />
</SectionCard>
</PageShell>
Props: - maxWidth — Container max width (xs, sm, md, lg, xl) - scrollable — Enable vertical scrolling
SectionCard¶
Groups related content with visual separation.
<SectionCard title={t("accountSettings")}>
<ProfileForm />
</SectionCard>
<SectionCard>
<DangerZone />
</SectionCard>
EmptyState¶
Placeholder when no content exists.
<EmptyState
icon={<MarkerIcon />}
title={t("noMarkers")}
description={t("noMarkersDescription")}
action={
<Button onClick={openCreate}>
{t("createFirst")}
</Button>
}
/>
Form Components¶
Specialized Fields¶
| Component | Location | Purpose |
|---|---|---|
ColorSelector | forms/selectors/ | Color selection from palette |
IconSelector | forms/selectors/ | Icon selection grid |
DatePickerField | forms/fields/ | Date selection |
MonthYearSelector | forms/selectors/ | Month/year navigation |
SelectField | forms/fields/ | Dropdown select |
SearchableSelectField | forms/fields/ | Searchable autocomplete |
SearchField | forms/fields/ | Search input with clear |
AccountSelectField | forms/fields/ | Budget account select |
CategorySelectField | forms/fields/ | Category select |
PasswordField | forms/fields/ | Password input with toggle |
Naming convention: Form field wrappers use *Field suffix; selectors use *Selector suffix.
ColorSelector¶
IconSelector¶
Button Patterns¶
Use custom Button component, not raw MUI:
import Button from "@/components/common/Button";
<Button variant="contained" color="primary">
{t("save")}
</Button>
<Button variant="outlined" color="error" loading={isDeleting}>
{t("delete")}
</Button>
Features: - Loading state with spinner - Disabled during loading - Consistent styling
List Patterns¶
Sortable Images¶
SortableImageGrid for drag-to-reorder image galleries with @dnd-kit/sortable.
Infinite Scroll¶
Use useInfiniteScroll hook with TanStack Query's useInfiniteQuery for paginated lists.
Error Boundary¶
Catches rendering errors with fallback UI.
<ErrorBoundary
fallback={<ErrorFallback onRetry={retry} />}
onError={(error) => captureException(error)}
>
<FeatureComponent />
</ErrorBoundary>
Development mode shows error details; production shows user-friendly message.
Loading States¶
Skeleton¶
Suspense¶
Component Organization¶
components/
├── common/ # Commonly used (Button, InfoTooltip)
├── display/ # Visual display (AmountDisplay, TruncatedText, UserAvatar)
├── feedback/ # User feedback (EmptyState, ErrorBoundary, ListItemSkeleton)
├── forms/
│ ├── fields/ # Field wrappers (*Field components)
│ ├── selectors/ # Selection UI (ColorSelector, IconSelector)
│ ├── security/ # Form security (Captcha, HoneypotField)
│ └── accessibility/ # Accessibility (FormErrorAnnouncer)
├── tables/ # Tabular data (PaginatedList, PaginatedTable)
├── images/ # Image handling (ImageCard, SortableImageCard)
├── dialogs/ # Modal dialogs (ConfirmDeleteDialog, UnsavedChangesDialog)
├── layout/ # Page layout (PageShell, SectionCard)
├── navigation/ # Navigation (HubButton)
├── pwa/ # PWA components (PwaUpdateDialog, PwaUpdateIndicator)
└── routing/ # Route guards (ProtectedRoute, GuestRoute)
Domain-specific components live in their page folders (e.g., pages/budget/components/).
Related¶
- Forms — Form patterns
- Dialogs — Dialog patterns
- State Management — Component state