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")} />
<MarkerList markers={markers} />
</PageShell>
Props: - maxWidth — Container max width (xs, sm, md, lg, xl) - scrollable — Enable vertical scrolling
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 | components/forms/ | Color selection from palette |
IconSelector | components/forms/ | Icon selection grid |
DatePickerField | components/forms/ | Date selection |
SelectField | components/forms/ | Dropdown select |
SearchSelectField | components/forms/ | Searchable autocomplete |
SearchField | components/forms/ | Search input with clear |
PasswordField | components/forms/ | Password input with toggle |
RichTextField | components/forms/ | Rich text editor |
MonthYearSelector | features/budget/components/ | Month/year navigation |
AccountSelectField | features/budget/components/ | Budget account select |
CategorySelectField | features/budget/components/ | Category select |
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/ui/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 the InfiniteScrollTrigger component (in components/feedback/) with TanStack Query's useInfiniteQuery for paginated lists — render it at the end of the list to load the next page when it scrolls into view.
Loading States¶
Skeletons¶
All skeleton components are consolidated in components/feedback/skeletons/:
| Component | Purpose |
|---|---|
FormSkeleton | Form placeholder |
ListItemSkeleton | Single list item placeholder |
ListSkeleton | Full list placeholder with configurable item count |
import ListSkeleton from "@/components/feedback/skeletons/ListSkeleton";
{isLoading ? <ListSkeleton count={5} /> : <ItemList items={items} />}
Suspense¶
Component Organization¶
components/
├── ui/ # Button, InfoTooltip, EmailStatusBadge, TruncatedText, UserAvatar,
│ # DonutChart, PaginatedList, PaginatedTable, OverlayIconButton
├── feedback/ # EmptyState, InfiniteScrollTrigger, PullToRefresh*, PwaUpdate*
│ ├── dialogs/ # ConfirmDeleteDialog, UnsavedChangesDialog
│ ├── skeletons/ # FormSkeleton, ListItemSkeleton, ListSkeleton
│ └── toast/ # Toast system
├── forms/ # FLAT: FormModal, FormErrorAnnouncer, *Field wrappers,
│ # ColorSelector, IconSelector, RichText*
├── images/ # ImageCard, SortableImageCard/Grid, ShimmerImage, ImageCropper,
│ # ImageUpload, FullscreenImageViewer
├── layout/ # PageShell, PageTransition, ErrorBoundary, ThemeWrapper,
│ # HubButton, ModalStackHandler
└── routing/ # Route guards (ProtectedRoute, GuestRoute)
Domain-specific components live in their feature folders (e.g., features/budget/components/AmountDisplay, features/settings/components/SectionCard, features/account/components/Captcha).
Related¶
- Forms — Form patterns
- Dialogs — Dialog patterns
- State Management — Component state