npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@simplix-react/ui

v0.2.2

Published

CRUD scaffolding UI components with explicit field components and compound patterns

Downloads

184

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>
  );
}

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 B

CLI 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.ts

Design 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 | | 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, Checkbox
  • Select (Root, Trigger, Value, Content, Item, Group, Label, Separator)
  • RadioGroup (Root, Item)
  • Calendar (date picker grid)
  • 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)
  • Separator
  • Skeleton

Form Field Components

All form fields follow the same pattern: value + onChange + common field props (label, error, description, required, disabled, className).

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.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, labelPosition, size | | DetailFieldWrapper | Wraps read-only display values with label | label, labelPosition, size |

import { FieldWrapper } from "@simplix-react/ui";

<FieldWrapper label="Custom Field" error={errors.custom} required>
  <MyCustomInput 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={{ labelPosition: "left", size: "sm" }}>
  <FormFields.TextField label="Name" value={name} onChange={setName} />
  <FormFields.TextField label="Email" value={email} onChange={setEmail} />
</FieldVariantContext.Provider>

Options:

  • labelPosition: "top" (default) | "left" | "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

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 }

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, filters, sort, pagination, selection, emptyReason }

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

  • All form fields generate unique IDs via useId() and associate labels via htmlFor
  • Hidden labels use sr-only class for screen reader accessibility
  • Error messages use role="alert" for live announcements
  • aria-invalid set on inputs when errors are present
  • aria-label provided when labelPosition="hidden"
  • Keyboard navigation support via useKeyboardNav hook
  • 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.