@simplix-react/ui
v0.3.8
Published
CRUD scaffolding UI components with explicit field components and compound patterns
Readme
@simplix-react/ui
CRUD scaffolding UI components with explicit field components and compound patterns.
Overview
@simplix-react/ui provides a headless-friendly, Tailwind CSS-based component library for building CRUD interfaces. Every component follows the No Magic principle: explicit value/onChange props, no hidden state, and full control at every level.
Quick Start
import {
UIProvider,
CrudForm,
FormFields,
CrudDetail,
DetailFields,
List,
useCrudList,
} from "@simplix-react/ui";
function App() {
return (
<UIProvider>
{/* Your CRUD pages */}
</UIProvider>
);
}Styles & Theme
Component CSS ships values only as var(--*) references. Import the
stylesheet plus the official token theme from your app's CSS entry:
@import "tailwindcss";
@import "@simplix-react/ui/styles.css"; /* component styles */
@import "@simplix-react/ui/theme.css"; /* official design tokens (opt-in) */
@source "../node_modules/@simplix-react/ui/dist/**/*.js";theme.css defines the design tokens (light/dark, color variants, radius,
typography, status indicator animations) consumed by the components. Apps may override any token by
redefining it after the import. Projects scaffolded via simplix init
wire this up automatically.
App Architecture (FSD)
simplix-react apps follow Feature-Sliced Design (FSD) — UI code lives in modules, not in domain packages:
my-app/
├── packages/ Domain packages (generated) — NO UI code
│ ├── user/
│ │ ├── schemas.ts Zod schemas
│ │ ├── contract.ts API contracts
│ │ ├── hooks.ts React Query hooks
│ │ └── form-hooks.ts TanStack Form hooks
│ └── product/
│ └── ...
│
├── modules/ FSD modules — compose domain packages + @simplix-react/ui
│ └── myapp-admin/
│ └── src/
│ ├── manifest.ts Module manifest (navigation, capabilities)
│ ├── index.ts Root exports
│ ├── features/ Feature layer — CRUD screens
│ │ ├── user/ uses @myapp/user hooks
│ │ │ ├── user-list.tsx
│ │ │ ├── user-form.tsx
│ │ │ ├── user-detail.tsx
│ │ │ └── index.ts
│ │ └── product/ uses @myapp/product hooks
│ │ └── ...
│ ├── widgets/ Widget layer — compose multiple features
│ │ └── dashboard/
│ ├── shared/ Shared layer
│ │ ├── ui/
│ │ ├── lib/
│ │ └── config/
│ └── locales/ i18n (optional)
│
└── app/ Final app — composes modules only
└── src/
├── features/
├── widgets/
└── shared/FSD Layer Rules
| Layer | Can import from | Cannot import from |
| --- | --- | --- |
| shared/ | external packages only | features/, widgets/ |
| features/ | shared/, domain packages | widgets/ |
| widgets/ | shared/, features/, domain packages | — |
Module can compose multiple domain packages
A single module can contain CRUD features from different domain packages:
// modules/myapp-admin/src/features/user/user-list.tsx
import { useUserHooks } from "@myapp/user"; // domain package A
// modules/myapp-admin/src/features/product/product-list.tsx
import { useProductHooks } from "@myapp/product"; // domain package BCLI Scaffold
# Generate CRUD feature into a module's features/ layer
simplix scaffold user --module myapp-admin
# Output: modules/myapp-admin/src/features/user/
# user-list.tsx, user-form.tsx, user-detail.tsx, index.tsDesign Principles
No Magic
Every field component requires explicit value and onChange props. There is no hidden form state, no automatic binding, and no implicit context dependencies.
// Explicit value/onChange — always
<FormFields.TextField
label="Name"
value={name}
onChange={setName}
required
/>Compound Components
CRUD layouts (List, CrudForm, CrudDetail) use the compound component pattern. Assemble only what you need:
<List>
<List.Toolbar>
<List.Search value={search} onChange={setSearch} />
</List.Toolbar>
<List.Table data={items} sort={sort} onSortChange={setSort}>
<List.Column field="name" header="Name" sortable />
<List.Column field="status" header="Status" display="badge" />
</List.Table>
<List.Pagination page={1} pageSize={10} total={100} totalPages={10} onPageChange={setPage} />
</List>Component Override Strategy
5 levels of customization, from least to most effort:
| Level | Method | Scope |
| --- | --- | --- |
| 1 | className prop | Single instance |
| 2 | CVA variants (variant, size) | Variant-level |
| 3 | FieldVariantContext | Section/page-wide field styling |
| 4 | UIProvider overrides | App-wide component replacement |
| 5 | Custom component | Full replacement |
// Level 4: Replace Input globally via UIProvider
import { MyCustomInput } from "./my-input";
<UIProvider overrides={{ Input: MyCustomInput }}>
{/* All TextField components now use MyCustomInput */}
</UIProvider>API Reference
Layout Primitives
Semantic layout components built with CVA variants.
| Component | Description | Key Props |
| --- | --- | --- |
| Stack | Vertical/horizontal flex layout | direction, gap, align, justify, wrap |
| Flex | Horizontal flex (alias for Stack direction="row") | Same as Stack |
| Grid | CSS Grid layout | columns (1-6), gap, gapX/gapY (per-axis overrides), divider (vertical line between columns), template (arbitrary grid-template-columns) |
| Container | Centered max-width wrapper | size (sm/md/lg/xl/full) |
| Section | Content section with title/description | title, description |
| Card | Card container with border and shadow | padding (none/sm/md/lg), interactive |
| Heading | Semantic heading (h1-h6) | level (1-6), tone, font (sans/display/mono) |
| Text | Body text with typography scale | size (lg/base/sm/caption), tone, font (sans/display/mono) |
<Stack gap="lg">
<Section title="User Info" description="Basic information">
<Grid columns={2} gap="md">
<FormFields.TextField label="First Name" value={first} onChange={setFirst} />
<FormFields.TextField label="Last Name" value={last} onChange={setLast} />
</Grid>
</Section>
</Stack>Base Components
Unstyled Radix UI primitives with Tailwind CSS styling. Used internally by field components and available for direct use.
Button(with CVA variants:variant,size)Input,Textarea,Label,Badge,Switch,CheckboxSelect(Root, Trigger, Value, Content, Item, Group, Label, Separator)RadioGroup(Root, Item)Calendar(date picker grid)TimePicker(time-of-day input with AM/PM toggle and drop-open hour/minute lists)Popover(Root, Trigger, Content, Anchor)Dialog(Root, Trigger, Content, Header, Footer, Title, Description, Close)DropdownMenu(Root, Trigger, Content, Item, CheckboxItem, RadioItem, RadioGroup, Label, Separator, Sub, SubTrigger, SubContent, Group)Sheet(Root, Trigger, Content, Header, Footer, Title, Description, Close)Table(Root, Header, Body, Footer, Head, Row, Cell, Caption)Tabs(Root, List, Trigger, Content)Tooltip(Provider, Root, Trigger, Content)NavigationMenu(Root, List, Item, Trigger, Content, Link, Viewport, Indicator)SeparatorSkeleton
Status & Display
Tone-driven components sharing a single semantic color vocabulary. Each StatusTone (success, warning, danger, info, neutral, pending, processing) bundles every Tailwind class — badge, dot, ring, icon, surface — with its dark: variant, so adopting them keeps colors consistent across light/dark.
StatusBadge— compact status pill (tone + label, optional leading dot/icon,filled/outlineappearance)StatusDot— small indicator dot with an optionalping/flashring animationAlertBanner— tinted status banner with icon, title/subtitle (or free-form children), trailing slot, anddefault/sm/hintdensityDetailList/DetailListRow— bordered key-value rows, optionally interactive (onClickupgrades a row to button semantics)LabeledField— label + description row paired with an arbitrary trailing controlSettingSwitch— labeled toggle row built onLabeledField+Switch
Tone tokens are exported directly for custom rendering: STATUS_TONES, STATUS_TONE_NAMES, statusToneClass(tone, slot), plus the StatusTone / StatusToneToken / IconComponent types.
<StatusBadge tone="success" label="Active" showDot />
<AlertBanner tone="danger" icon={AlertTriangleIcon} title="Connection lost" subtitle="Reconnecting…" />Form Field Components
All form fields follow the same pattern: value + onChange + common field props (label, error, description, required, disabled, className).
Every field also accepts prefixControl / suffixControl — a control rendered on the same row as the input (e.g. an add button, IconPicker, ColorPicker). Use these instead of composing a button next to the field: the control stays aligned with the input while description and error render below at full width.
<FormFields.TextField
label="Training key"
value={draft}
onChange={setDraft}
description="Added on submit."
suffixControl={<Button onClick={add}>Add</Button>}
/>import { FormFields } from "@simplix-react/ui";| Component | Value Type | Key Props |
| --- | --- | --- |
| FormFields.TextField | string | type (text/email/url/password/tel), placeholder, maxLength |
| FormFields.TextareaField | string | rows, maxLength, resize (none/vertical/both) |
| FormFields.NumberField | number \| null | min, max, step, placeholder |
| FormFields.SelectField | string | options (label/value pairs), placeholder |
| FormFields.SwitchField | boolean | switchProps |
| FormFields.CheckboxField | boolean | checkboxProps |
| FormFields.RadioGroupField | string | options (label/value/description), direction |
| FormFields.DateField | Date \| null | minDate, maxDate, format, placeholder |
| FormFields.DateTimeField | Date \| string \| number \| null | minDate, maxDate, hideTime, hour12, minuteStep |
| FormFields.TimeField | TimeValue \| null ({ hours, minutes }) | minTime, maxTime, hour12, minuteStep |
| FormFields.ComboboxField | string \| null | options, onSearch, loading, emptyMessage |
| FormFields.PasswordField | string | placeholder, maxLength (with visibility toggle) |
| FormFields.ColorField | string (hex) | Native color picker + hex text input |
| FormFields.SliderField | number | min, max, step, showValue |
| FormFields.MultiSelectField | string[] | options, placeholder, maxCount |
| FormFields.Field | ReactNode (children) | Generic wrapper for custom content |
<FormFields.SelectField
label="Status"
value={status}
onChange={setStatus}
options={[
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
]}
required
error={errors.status}
/>Detail Field Components
Read-only display fields with formatting capabilities.
import { DetailFields } from "@simplix-react/ui";| Component | Value Type | Key Props |
| --- | --- | --- |
| DetailFields.DetailTextField | string \| null | fallback, copyable |
| DetailFields.DetailNumberField | number \| null | format (decimal/currency/percent), locale, currency |
| DetailFields.DetailDateField | Date \| string \| null | format (date/datetime/relative), fallback |
| DetailFields.DetailBadgeField | string | variants (value-to-variant mapping) |
| DetailFields.DetailLinkField | string | href, external |
| DetailFields.DetailBooleanField | boolean \| null | mode (text/icon), labels |
| DetailFields.DetailImageField | string \| null (URL) | alt, width, height, imageClassName |
| DetailFields.DetailListField | string[] \| null | mode (badges/comma/bullet) |
| DetailFields.DetailField | ReactNode (children) | Generic wrapper for custom content |
<DetailFields.DetailDateField
label="Created At"
value={user.createdAt}
format="relative"
/>
<DetailFields.DetailBadgeField
label="Status"
value={user.status}
variants={{ active: "success", inactive: "secondary", banned: "destructive" }}
/>Field Wrappers
Low-level wrappers used internally by FormFields and DetailFields. Export them to build custom field components.
| Component | Purpose | Key Props |
| --- | --- | --- |
| FieldWrapper | Wraps editable inputs with label, error, description | label, error, description, required, disabled, layout, size |
| DetailFieldWrapper | Wraps read-only display values with label | label, layout, size |
import { FieldWrapper } from "@simplix-react/ui";
// Pass a render function to receive { id, labelId }. Put `id` on a labelable
// control so the label names it; use `labelId` with `aria-labelledby` for a
// composite control (radio group, date picker) with no single labelable element.
// A plain-element child is instead named by the wrapping fieldset.
<FieldWrapper label="Custom Field" error={errors.custom} required>
{({ id }) => <MyCustomInput id={id} value={value} onChange={onChange} />}
</FieldWrapper>Field Variant System
Control field label position and size across a section or page using context:
import { FieldVariantContext } from "@simplix-react/ui";
// All fields inside will use left-aligned labels at small size
<FieldVariantContext.Provider value={{ layout: "left", size: "sm" }}>
<FormFields.TextField label="Name" value={name} onChange={setName} />
<FormFields.TextField label="Email" value={email} onChange={setEmail} />
</FieldVariantContext.Provider>Options:
layout:"top"(default) |"left"|"inline"|"trailing"(control right-aligned with a dashed leader line from the label — theSwitchFielddefault) |"hidden"(sr-only for accessibility)size:"sm"|"md"(default) |"lg"
CRUD Layout Components
List (Compound)
Full-featured data table with sorting, filtering, pagination, selection, and bulk actions.
const { data, filters, sort, pagination, selection, emptyReason } = useCrudList(useUserList);
<List>
<List.Toolbar>
<List.Search value={filters.search} onChange={filters.setSearch} />
<List.Filter
label="Role"
value={roleFilter}
onChange={setRoleFilter}
options={roleOptions}
/>
</List.Toolbar>
<List.BulkActions selectedCount={selection.selected.size} onClear={selection.clear}>
<List.BulkAction label="Delete Selected" onClick={handleBulkDelete} variant="destructive" />
</List.BulkActions>
{emptyReason ? (
<List.Empty reason={emptyReason} />
) : (
<List.Table
data={data}
sort={{ field: sort.field!, direction: sort.direction }}
onSortChange={(s) => sort.setSort(s.field, s.direction)}
selectable
selectedIndices={selection.selected}
onSelectionChange={selection.toggle}
onSelectAll={() => selection.toggleAll(data)}
onRowClick={handleRowClick}
>
<List.Column field="name" header="Name" sortable />
<List.Column field="email" header="Email" sortable />
<List.Column field="status" header="Status" display="badge"
variants={{ active: "success", inactive: "secondary" }} />
<List.Column field="createdAt" header="Created" format="date" sortable />
</List.Table>
)}
<List.Pagination
page={pagination.page}
pageSize={pagination.pageSize}
total={pagination.total}
totalPages={pagination.totalPages}
onPageChange={pagination.setPage}
onPageSizeChange={pagination.setPageSize}
/>
</List>Sub-components: List.Toolbar, List.Search, List.Filter, List.Table, List.Column, List.RowActions, List.Action, List.Pagination, List.BulkActions, List.BulkAction, List.Empty
List.Table and Tree.Table render a sticky header by default — once scrolling an outer container (a page, dialog, or detail-pane body) would hide the header row, it sticks to the top of the nearest scrollable ancestor. Wide tables keep their own horizontal scrollbar, and the floating header scrolls with its columns. Pass stickyHeader={false} to disable. Inside a List.TableCard the table owns its scroll region and the header is always sticky.
CardList
Mobile-friendly card-based layout alternative to table.
<CardList
data={items}
columns={2}
renderCard={(item, index) => (
<div key={index} className="rounded-lg border p-4">
<h3>{item.name}</h3>
</div>
)}
/>CrudForm (Compound)
Form layout with sections and actions.
<CrudForm
onSubmit={handleSubmit}
fieldVariant={{ labelPosition: "top", size: "md" }}
warnOnUnsavedChanges
>
<CrudForm.Section title="Basic Info" layout="two-column">
<FormFields.TextField label="Name" value={name} onChange={setName} required />
<FormFields.TextField label="Email" value={email} onChange={setEmail} type="email" />
</CrudForm.Section>
<CrudForm.Section title="Details" layout="single-column">
<FormFields.TextareaField label="Bio" value={bio} onChange={setBio} />
</CrudForm.Section>
<CrudForm.Actions>
<button type="button" onClick={onCancel}>Cancel</button>
<button type="submit">Save</button>
</CrudForm.Actions>
</CrudForm>Sub-components: CrudForm.Section (with layout: single-column/two-column/three-column), CrudForm.Actions
CrudDetail (Compound)
Read-only detail view layout.
<CrudDetail fieldVariant={{ labelPosition: "left" }}>
<CrudDetail.Section title="User Info">
<DetailFields.DetailTextField label="Name" value={user.name} copyable />
<DetailFields.DetailDateField label="Joined" value={user.createdAt} format="relative" />
<DetailFields.DetailBadgeField
label="Status"
value={user.status}
variants={{ active: "success", inactive: "secondary" }}
/>
</CrudDetail.Section>
<CrudDetail.Actions>
<button onClick={onEdit}>Edit</button>
<button onClick={onDelete}>Delete</button>
</CrudDetail.Actions>
</CrudDetail>Sub-components: CrudDetail.Section, CrudDetail.Actions
CrudDelete
Confirmation dialog for delete operations using Radix AlertDialog.
import { CrudDelete } from "@simplix-react/ui";
<CrudDelete
open={showDelete}
onOpenChange={setShowDelete}
onConfirm={handleDelete}
entityName="user"
loading={isDeleting}
/>Props: open, onOpenChange, onConfirm, title, description, loading, entityName
CrudErrorBoundary
Error boundary for catching render errors in CRUD components.
import { CrudErrorBoundary } from "@simplix-react/ui";
<CrudErrorBoundary
fallback={(error, reset) => (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
onError={(error) => logError(error)}
>
<UserList />
</CrudErrorBoundary>Props: children, fallback (ReactNode or render function), onError
Wizard (Compound)
Multi-step form wizard with step indicator and validation.
import { Wizard } from "@simplix-react/ui";
<Wizard onComplete={handleSubmit}>
<Wizard.Step title="Basic Info" validate={validateStep1}>
<FormFields.TextField label="Name" value={name} onChange={setName} />
</Wizard.Step>
<Wizard.Step title="Details">
<FormFields.TextareaField label="Bio" value={bio} onChange={setBio} />
</Wizard.Step>
<Wizard.Step title="Review">
<p>Confirm your details before submitting.</p>
</Wizard.Step>
</Wizard>Sub-components: Wizard.Step (with title, description, and optional async validate function)
QueryFallback
Loading and not-found fallback guard for single-entity views.
import { QueryFallback } from "@simplix-react/ui";
const { data, isLoading } = useGet(id);
if (isLoading || !data) return <QueryFallback isLoading={isLoading} notFoundMessage="Pet not found." />;Props: isLoading, notFoundMessage, loadingMessage
CRUD Hooks
useCrudFormSubmit
Handles create/update mutation dispatch for CRUD forms. Determines whether to create or update based on entityId presence.
const { isEdit, handleSubmit, isPending } = useCrudFormSubmit<FormValues>({
entityId,
create: entityHooks.useCreate(),
update: entityHooks.useUpdate(),
onSuccess: () => navigate(-1),
});Returns: { isEdit, handleSubmit, isPending, fieldErrors }
Client-side validation (optional)
Pass a validator to run a sync check on the raw form values before the
server mutation. Returning a non-empty Record<field, message> stops the
submit and exposes the errors via fieldErrors; the create/update mutation
is not called. Returning null (or {}) passes through to the server.
Client and server errors are temporally mutually exclusive on the same
submit, so fieldErrors always reflects exactly one source per attempt
(client → server → success replaces).
// Zod (use `zodToFieldErrors` from @simplix-react/form):
import { zodToFieldErrors } from "@simplix-react/form";
import { createUserSchema, updateUserSchema } from "@my-app/domain-user";
const { handleSubmit, fieldErrors } = useCrudFormSubmit<FormValues>({
entityId,
create: adaptOrvalCreate(_create),
update: adaptOrvalUpdate(_update, "userId"),
validator: (v) => zodToFieldErrors(isEdit ? updateUserSchema : createUserSchema, v),
onSuccess,
});
// Custom imperative rules:
validator: (v) => v.email?.includes("@") ? null : { email: "Invalid email" }Wrap inline validator functions with useCallback / useMemo to keep
handleSubmit identity stable across renders.
useCrudDeleteList
Manages delete-confirmation state for list views.
const del = useCrudDeleteList();
// Trigger: del.requestDelete({ id: row.id, name: row.name })
<CrudDelete
open={del.open}
onOpenChange={(o) => { if (!o) del.cancel(); }}
onConfirm={() => deleteMutation.mutate(del.target!.id)}
entityName={del.target?.name}
/>Returns: { open, target, requestDelete, cancel }
useCrudDeleteDetail
Manages delete-confirmation state for detail views (single item).
const del = useCrudDeleteDetail();
<button onClick={del.requestDelete}>Delete</button>
<CrudDelete open={del.open} onOpenChange={del.onOpenChange} onConfirm={handleDelete} />Returns: { open, requestDelete, cancel, onOpenChange }
usePreviousData
Retains previous query data during refetch to prevent layout flicker.
const { data, isLoading } = useGet(id);
const stableData = usePreviousData(data);useFadeTransition
CSS fade transition hook for view transitions (e.g., list-to-detail).
const { isVisible, shouldRender } = useFadeTransition({
show: !!selectedId,
duration: 200,
});Returns: { isVisible, shouldRender }
useListDetailState
State management for the ListDetail pattern — tracks selected item and panel toggle.
const { selectedId, select, deselect, isDetailOpen } = useListDetailState({
onSelect: (id) => navigate(`/users/${id}`),
});Returns: { selectedId, select, deselect, isDetailOpen }
useCrudList
State management hook for list views with filtering, sorting, pagination, and selection.
const result = useCrudList(useUserList, {
stateMode: "server", // "server" (API-driven) or "client" (local)
defaultSort: { field: "name", direction: "asc" },
defaultPageSize: 20,
});Returns: { data, isLoading, error, isPaused, failureCount, filters, sort, pagination, selection, emptyReason }
emptyReason is "no-data", "no-filter", "no-search", "error",
"unavailable", or null. "unavailable" covers a query that is neither
settled successfully nor settled with an error — paused (React Query
fetchStatus === "paused", which happens while the document is hidden or the
browser reports offline) or waiting to retry after a failed attempt. Such a
query reports isLoading: false and error: null, so it is reported as "the
list could not be loaded" rather than as an empty result.
useUrlSync
Syncs list state (filters, sort, pagination) with URL query parameters.
useUrlSync({
filters: { search, values: filterValues },
sort: sortState,
pagination: { page, pageSize, total },
setFilters,
setSort,
setPage,
});useVirtualList
Virtual scrolling for large lists via @tanstack/react-virtual.
const { virtualizer, virtualRows, totalHeight } = useVirtualList({
count: data.length,
estimateSize: () => 48,
parentRef: scrollRef,
overscan: 5,
});useKeyboardNav
Keyboard navigation for list components (ArrowUp/Down, Enter, Space, Ctrl+K, Escape).
const { containerRef } = useKeyboardNav({
onNavigate: (dir) => { /* move focus up/down */ },
onSelect: () => { /* select current item */ },
onToggle: () => { /* toggle checkbox */ },
onSearch: () => { /* focus search input */ },
onEscape: () => { /* dismiss */ },
});useMediaQuery
Responsive breakpoint detection via matchMedia API.
const isMobile = useMediaQuery("(max-width: 768px)");useAutosave
Debounced autosave hook that watches form values for changes.
const { status, lastSavedAt, isSaving } = useAutosave({
values: formValues,
onSave: async (values) => await api.saveDraft(values),
debounceMs: 2000,
hasErrors: form.hasErrors,
});Returns: { status, lastSavedAt, isSaving }
Status values: "idle" | "saving" | "saved" | "error"
CRUD Patterns
ListDetail
Two rendering variants for list + detail layouts.
Panel variant (default): Side-by-side layout with draggable divider.
<ListDetail variant="panel" listWidth="1/3">
<ListDetail.List>
{/* List content */}
</ListDetail.List>
<ListDetail.Detail>
{/* Detail content */}
</ListDetail.Detail>
</ListDetail>Dialog variant: Full-width list with detail in a modal dialog.
<ListDetail variant="dialog" onClose={() => setSelected(null)}>
<ListDetail.List>
{/* List takes full width */}
</ListDetail.List>
<ListDetail.Detail>
{/* Opens in a modal dialog */}
</ListDetail.Detail>
</ListDetail>Props:
variant:"panel"(default) |"dialog"listWidth:"1/4"|"1/3"|"2/5"|"1/2"|"3/5"|"2/3"|"3/4"|"4/5"(panel variant only)activePanel:"list"|"detail"(controlled mode)onClose: Callback when dialog is dismissed (dialog variant only)
Panel variant features:
- Responsive: collapses to single-panel on mobile
- Draggable divider with keyboard support (ArrowLeft/Right)
ListDetailRoot is the same component as ListDetail — exported as an alias for compound pattern composition.
Router Adapters
The package is router-agnostic. Use CrudProvider to inject a router adapter.
import { CrudProvider, createReactRouterAdapter } from "@simplix-react/ui";
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
function AppShell() {
const adapter = createReactRouterAdapter({ useNavigate, useSearchParams, useLocation });
return (
<CrudProvider router={adapter}>
{/* CRUD pages can now use useRouter(), useUrlSync(), etc. */}
</CrudProvider>
);
}UIProvider
Override base components globally. Supports nesting for scoped overrides.
import { UIProvider } from "@simplix-react/ui";
import { MyInput, MySelect } from "./custom-components";
<UIProvider overrides={{
Input: MyInput,
Select: {
Root: MySelect.Root,
Trigger: MySelect.Trigger,
Value: MySelect.Value,
Content: MySelect.Content,
Item: MySelect.Item,
},
}}>
{/* All fields now use custom input/select */}
</UIProvider>Overridable components: Input, Textarea, Label, Switch, Checkbox, Badge, Calendar, Select (compound), RadioGroup (compound).
Utilities
| Function | Description |
| --- | --- |
| cn(...inputs) | Merges class names with clsx + tailwind-merge |
| toTestId(label) | Converts a label string to a kebab-case data-testid value |
| sanitizeHtml(dirty) | Sanitizes HTML via DOMPurify |
Accessibility
FieldWrappergenerates the field's ids and hands{ id, labelId }to a render-function child, so every control gets an accessible name:idplus the label'shtmlForfor a labelable control, oraria-labelledby={labelId}for a composite one. A plain-element child is named by the wrapping fieldset instead.- Icon-only controls (row actions, toolbar buttons) carry their own
aria-label; a tooltip describes a control but does not name one. - Hidden labels use
sr-onlyclass for screen reader accessibility - Error messages use
role="alert"for live announcements aria-invalidset on inputs when errors are presentaria-labelprovided whenlayout="hidden"- Keyboard navigation support via
useKeyboardNavhook - Sort buttons and pagination controls have descriptive
aria-label - Selection checkboxes have row-specific
aria-label
Peer Dependencies
{
"@simplix-react/form": "workspace:*",
"@simplix-react/i18n": "workspace:*",
"@simplix-react/react": "workspace:*",
"react": ">=18.0.0",
"react-router-dom": ">=6.0.0 (optional)"
}react-router-dom is required only when using createReactRouterAdapter. Other router adapters can be provided via CrudProvider.
License
See root LICENSE file.
