cx@crudx/shadcn · Components
@crudx/shadcn

shadcn/ui component reference

Every component exported by @crudx/shadcn, rendered live with a copy-pasteable snippet. API-compatible with @crudx/mui — same props, same names, same callback shapes.

BreadcrumbView

import { BreadcrumbView } from '@crudx/shadcn';

Navigation trail with optional icon + custom separator. Pass `current` to mark the active path.

tsx
<BreadcrumbView
  items={[
    { label: 'Home', url: '/', icon: <Home className="h-3.5 w-3.5" /> },
    { label: 'Team', url: '/team' },
    { label: 'Ada Lovelace' },
  ]}
  current="/team/ada"
  separator="/"
/>

ButtonDropdown

import { ButtonDropdown } from '@crudx/shadcn';

Menu-style dropdown with either button or icon trigger. Each item dispatches its `key` through `onItemClick`.

Last:
tsx
<div className="flex items-center gap-3">
  <ButtonDropdown
    type="button"
    items={[
      { key: 'edit', title: 'Edit' },
      { key: 'duplicate', title: 'Duplicate' },
      { key: 'archive', title: 'Archive' },
    ]}
    onItemClick={setLast}
  >
    Actions
  </ButtonDropdown>
  <span className="text-xs text-muted-foreground">
    Last: <code>{last ?? '—'}</code>
  </span>
</div>

Dialog

import { Dialog, DialogRefProps } from '@crudx/shadcn';

Imperative dialog driven by ref (`open()`, `close()`, `toggle()`). Built-in `confirmation`, `info`, `success`, `error`, `warning`, and `custom` variants.

Last action:
tsx
<div className="flex items-center gap-3">
  <button
    type="button"
    className="inline-flex h-9 items-center justify-center rounded-md border border-border px-3 text-sm font-medium text-foreground hover:bg-accent"
    onClick={() => ref.current?.open()}
  >
    Open dialog
  </button>
  <span className="text-xs text-muted-foreground">
    Last action: <code>{result}</code>
  </span>
  <Dialog
    ref={ref}
    type="confirmation"
    title="Delete this record?"
    message="This action can't be undone. Are you sure you want to continue?"
    primaryText="Delete"
    secondaryText="Cancel"
    onClickPrimaryAction={() => setResult('confirmed')}
    onClickSecondaryAction={() => setResult('cancelled')}
  />
</div>

NumberFormatView

import { NumberFormatView } from '@crudx/shadcn';

Number formatter wrapper. Uses numeral.js format strings and supports prefix / postfix slots.

$1,234,567.89 USD74.2%2.5m
tsx
<div className="flex items-center gap-6">
  <NumberFormatView
    amount={1234567.89}
    format="0,0.00"
    prefix="$"
    postfix=" USD"
  />
  <NumberFormatView amount={0.7421} format="0.0%" />
  <NumberFormatView amount={2_500_000} format="0.0a" prefix="≈ " />
</div>

RenderFlexView

import { RenderFlexView } from '@crudx/shadcn';

Declarative grid layout. The shadcn variant uses Tailwind col-span classes under the hood, but keeps the `xs/sm/md` API of the MUI variant.

Cell A
Cell B
Full-width row
tsx
<RenderFlexView
  containerProps={{ spacing: 2 }}
  items={[
    [
      {
        xs: 12,
        sm: 6,
        children: (
          <div className="rounded-md border border-border bg-card p-3 text-sm">
            Cell A
          </div>
        ),
      },
      {
        xs: 12,
        sm: 6,
        children: (
          <div className="rounded-md border border-border bg-card p-3 text-sm">
            Cell B
          </div>
        ),
      },
    ],
    [
      {
        xs: 12,
        children: (
          <div className="rounded-md border border-border bg-card p-3 text-sm">
            Full-width row
          </div>
        ),
      },
    ],
  ]}
/>

RenderNodeView

import { RenderNodeView } from '@crudx/shadcn';

Inline horizontal stack with a keyed list of nodes. Exposes `direction`, `alignItems`, and `gap` as first-class props.

23 favourites
tsx
<RenderNodeView
  direction="row"
  alignItems="center"
  gap={2}
  items={[
    {
      key: 'icon',
      content: <Heart className="h-4 w-4 text-red-500" />,
    },
    { key: 'label', content: <span>23 favourites</span> },
  ]}
/>

Table

import { Table } from '@crudx/shadcn';

The full table primitive — head, rows and pagination wired up. Use this when you want a turnkey table without going through CrudTableView.

IDROLESALARY
1Ada LovelaceEngineer$95,000
2Alan TuringResearcher$110,000
3Grace HopperArchitect$120,000
Rows per page
1-3 of 3
tsx
type Row = { id: number; name: string; role: string; salary: number };

const SAMPLE_ROWS: Row[] = [
  { id: 1, name: 'Ada Lovelace', role: 'Engineer', salary: 95000 },
  { id: 2, name: 'Alan Turing', role: 'Researcher', salary: 110000 },
  { id: 3, name: 'Grace Hopper', role: 'Architect', salary: 120000 },
];

const SAMPLE_COLUMNS = [
  { key: 'id', title: 'ID', width: 60, dataIndex: 'id' as const },
  { key: 'name', title: 'Name', dataIndex: 'name' as const, sortable: true },
  { key: 'role', title: 'Role', dataIndex: 'role' as const },
  {
    key: 'salary',
    title: 'Salary',
    align: 'right' as const,
    render: (_: unknown, record: Row) => (
      <NumberFormatView amount={record.salary} format="0,0" prefix="$" />
    ),
  },
];

<Table<Row>
  data={SAMPLE_ROWS}
  columns={SAMPLE_COLUMNS}
  striped
  bordered
  page={page}
  pageSize={pageSize}
  total={SAMPLE_ROWS.length}
  onPageChange={setPage}
  onPageSizeChange={setPageSize}
/>

Table — sticky columns

import { Table } from '@crudx/shadcn';

Pin the checkbox column to the left edge with `checkbox.sticky` and pin a column (e.g. an action column) to the right edge with `sticky: true`. Cumulative offsets and inset boundary shadows are applied automatically.

Scroll horizontally — the checkbox column stays pinned to the left and the action column stays pinned to the right. Inset shadows mark the sticky boundaries.

#EMAILTEAMROLECITYCOUNTRYJOINEDSALARYSTATUSACTION
1Ada Lovelaceada@analytical.engineEngineeringPrincipal EngineerLondonUnited Kingdom1843-12-10$215,000Active
2Alan Turingalan@bletchley.ukResearchCryptographerManchesterUnited Kingdom1936-06-12$198,000Active
3Grace Hoppergrace@navy.milCompilersRear AdmiralArlingtonUnited States1944-07-02$184,000Active
4Katherine Johnsonkatherine@nasa.govTrajectoryMathematicianHamptonUnited States1953-06-18$172,000Active
5Hedy Lamarrhedy@spread.spectrumCommsInventorViennaAustria1942-08-11$165,000Pending
6Tim Berners-Leetimbl@cern.chWeb PlatformDirectorGenevaSwitzerland1989-03-12$240,000Active
7Margaret Hamiltonmargaret@mit.eduApolloSoftware LeadCambridgeUnited States1965-09-01$202,000On leave
tsx
type StickyRow = {
  id: number;
  name: string;
  email: string;
  team: string;
  role: string;
  city: string;
  country: string;
  joined: string;
  salary: number;
  status: string;
};

const STICKY_ROWS: StickyRow[] = [
  { id: 1, name: 'Ada Lovelace', email: 'ada@analytical.engine', team: 'Engineering', role: 'Principal Engineer', city: 'London', country: 'United Kingdom', joined: '1843-12-10', salary: 215000, status: 'Active' },
  { id: 2, name: 'Alan Turing', email: 'alan@bletchley.uk', team: 'Research', role: 'Cryptographer', city: 'Manchester', country: 'United Kingdom', joined: '1936-06-12', salary: 198000, status: 'Active' },
  { id: 3, name: 'Grace Hopper', email: 'grace@navy.mil', team: 'Compilers', role: 'Rear Admiral', city: 'Arlington', country: 'United States', joined: '1944-07-02', salary: 184000, status: 'Active' },
  { id: 4, name: 'Katherine Johnson', email: 'katherine@nasa.gov', team: 'Trajectory', role: 'Mathematician', city: 'Hampton', country: 'United States', joined: '1953-06-18', salary: 172000, status: 'Active' },
  { id: 5, name: 'Hedy Lamarr', email: 'hedy@spread.spectrum', team: 'Comms', role: 'Inventor', city: 'Vienna', country: 'Austria', joined: '1942-08-11', salary: 165000, status: 'Pending' },
  { id: 6, name: 'Tim Berners-Lee', email: 'timbl@cern.ch', team: 'Web Platform', role: 'Director', city: 'Geneva', country: 'Switzerland', joined: '1989-03-12', salary: 240000, status: 'Active' },
  { id: 7, name: 'Margaret Hamilton', email: 'margaret@mit.edu', team: 'Apollo', role: 'Software Lead', city: 'Cambridge', country: 'United States', joined: '1965-09-01', salary: 202000, status: 'On leave' },
];

const STICKY_COLUMNS = [
  { key: 'id', title: '#', width: 60, dataIndex: 'id' as const, align: 'center' as const },
  { key: 'name', title: 'Name', width: 180, dataIndex: 'name' as const, sortable: true },
  { key: 'email', title: 'Email', width: 240, dataIndex: 'email' as const },
  { key: 'team', title: 'Team', width: 140, dataIndex: 'team' as const },
  { key: 'role', title: 'Role', width: 200, dataIndex: 'role' as const },
  { key: 'city', title: 'City', width: 150, dataIndex: 'city' as const },
  { key: 'country', title: 'Country', width: 180, dataIndex: 'country' as const },
  { key: 'joined', title: 'Joined', width: 130, dataIndex: 'joined' as const },
  {
    key: 'salary',
    title: 'Salary',
    width: 130,
    align: 'right' as const,
    render: (_: unknown, record: StickyRow) => (
      <NumberFormatView amount={record.salary} format="0,0" prefix="$" />
    ),
  },
  { key: 'status', title: 'Status', width: 120, dataIndex: 'status' as const },
  {
    key: 'action',
    title: 'Action',
    width: 110,
    sticky: true,
    align: 'center' as const,
    render: () => (
      <div className="flex items-center justify-center gap-1">
        <button
          type="button"
          aria-label="Edit"
          className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-border text-muted-foreground hover:bg-accent hover:text-foreground"
        >
          <Edit className="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          aria-label="Delete"
          className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-border text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
        >
          <Trash2 className="h-3.5 w-3.5" />
        </button>
      </div>
    ),
  },
];

<div className="max-w-full">
  <p className="mb-2 text-xs text-muted-foreground">
    Scroll horizontally  the checkbox column stays pinned to the left and
    the action column stays pinned to the right. Inset shadows mark the
    sticky boundaries.
  </p>
  <Table<StickyRow>
    data={STICKY_ROWS}
    columns={STICKY_COLUMNS}
    bordered
    pagination={false}
    checkbox={{
      enabled: true,
      sticky: true,
      dataIndex: 'id',
    }}
  />
</div>

TableHead

import { TableHead } from '@crudx/shadcn';

Standalone table header — use it when composing your own `<table>` instead of using `Table`. Drives sort indicators and the bulk-checkbox state.

IDROLESALARY
tsx
type Row = { id: number; name: string; role: string; salary: number };

const SAMPLE_COLUMNS = [
  { key: 'id', title: 'ID', width: 60, dataIndex: 'id' as const },
  { key: 'name', title: 'Name', dataIndex: 'name' as const, sortable: true },
  { key: 'role', title: 'Role', dataIndex: 'role' as const },
  {
    key: 'salary',
    title: 'Salary',
    align: 'right' as const,
    render: (_: unknown, record: Row) => (
      <NumberFormatView amount={record.salary} format="0,0" prefix="$" />
    ),
  },
];

<table className="w-full border-collapse">
  <TableHead<Row>
    columns={SAMPLE_COLUMNS}
    checkbox={{ enabled: true }}
    checked="partial"
    sorting={{ defaultOrder: 'name', defaultDirection: 'asc' }}
  />
</table>

TableRow

import { TableRow } from '@crudx/shadcn';

Standalone table row — pair with TableHead when you need a custom shell. Renders cells from the same column config as Table.

1Ada LovelaceEngineer$95,000
2Alan TuringResearcher$110,000
3Grace HopperArchitect$120,000
tsx
type Row = { id: number; name: string; role: string; salary: number };

const SAMPLE_ROWS: Row[] = [
  { id: 1, name: 'Ada Lovelace', role: 'Engineer', salary: 95000 },
  { id: 2, name: 'Alan Turing', role: 'Researcher', salary: 110000 },
  { id: 3, name: 'Grace Hopper', role: 'Architect', salary: 120000 },
];

const SAMPLE_COLUMNS = [
  { key: 'id', title: 'ID', width: 60, dataIndex: 'id' as const },
  { key: 'name', title: 'Name', dataIndex: 'name' as const, sortable: true },
  { key: 'role', title: 'Role', dataIndex: 'role' as const },
  {
    key: 'salary',
    title: 'Salary',
    align: 'right' as const,
    render: (_: unknown, record: Row) => (
      <NumberFormatView amount={record.salary} format="0,0" prefix="$" />
    ),
  },
];

<table className="w-full border-collapse">
  <tbody>
    {SAMPLE_ROWS.map((row, idx) => (
      <TableRow<Row>
        key={row.id}
        position={idx}
        data={row}
        columns={SAMPLE_COLUMNS}
      />
    ))}
  </tbody>
</table>

TablePagination

import { TablePagination } from '@crudx/shadcn';

Page controls. Note that the shadcn variant is 1-indexed (the MUI variant is 0-indexed) and exposes label slots for i18n.

Rows per page
1-10 of 142
tsx
<TablePagination
  page={page}
  pageSize={pageSize}
  total={142}
  pageSizeOptions={[10, 25, 50]}
  onPageChange={setPage}
  onPageSizeChange={setPageSize}
/>

TableSelectedBulkOptions

import { TableSelectedBulkOptions } from '@crudx/shadcn';

Bulk-action menu shown above a table when rows are selected. Defaults to a `{count} Item(s) Selected` label.

Last:
tsx
<div className="flex items-center gap-3">
  <TableSelectedBulkOptions
    total={3}
    items={[
      { key: 'delete', title: 'Delete selected' },
      { key: 'export', title: 'Export selected' },
    ]}
    onChange={(key: string) => setLast(key)}
  />
  <span className="text-xs text-muted-foreground">
    Last: <code>{last ?? '—'}</code>
  </span>
</div>

TableSettingsDensityOptions

import { TableSettingsDensityOptions } from '@crudx/shadcn';

Density picker. Three preset rows — default / small / medium — with a localisable label per option.

Density: default
tsx
<div className="flex items-center gap-3">
  <TableSettingsDensityOptions
    onChange={(key: string) => setDensity(key)}
  />
  <span className="text-xs text-muted-foreground">
    Density: <code>{density}</code>
  </span>
</div>

TableSettingsOptions

import { TableSettingsOptions } from '@crudx/shadcn';

Generic settings dropdown for table-level actions (column toggles, exports, …). Same item shape as ButtonDropdown.

Last:
tsx
<div className="flex items-center gap-3">
  <TableSettingsOptions
    items={[
      { key: 'columns', title: 'Manage columns' },
      { key: 'export', title: 'Export CSV' },
      { key: 'reset', title: 'Reset view' },
    ]}
    onChange={(key: string) => setLast(key)}
  />
  <span className="text-xs text-muted-foreground">
    Last: <code>{last ?? '—'}</code>
  </span>
</div>

TableSettingsSortingOptions

import { TableSettingsSortingOptions, SortingOptionType } from '@crudx/shadcn';

Three-state global sort toggle (DEFAULT / ASC / DESC). Pair with a sortable list view that doesn't pin sort to a single column.

Sort: DEFAULT
tsx
<div className="flex items-center gap-3">
  <TableSettingsSortingOptions
    selected={selected}
    onChange={(key: string) => setSelected(key as SortingOptionType)}
  />
  <span className="text-xs text-muted-foreground">
    Sort: <code>{selected}</code>
  </span>
</div>

TabView

import { TabView } from '@crudx/shadcn';

Tab list with content slots. Pass `content` per item, or use `renderContent` for fully controlled rendering.

tsx
<TabView
  items={[
    {
      key: 'overview',
      label: 'Overview',
      content: (
        <div className="p-3 text-sm">
          Overview content rendered for the active tab.
        </div>
      ),
    },
    {
      key: 'activity',
      label: 'Activity',
      content: <div className="p-3 text-sm">No recent activity.</div>,
    },
    {
      key: 'settings',
      label: 'Settings',
      content: <div className="p-3 text-sm">Settings tab content.</div>,
    },
  ]}
/>

TooltipView

import { TooltipView } from '@crudx/shadcn';

Tooltip wrapper around an arbitrary trigger. Disabled automatically when `title` is empty unless you force `enabled`. Provider is included internally.

tsx
<div className="flex items-center gap-3">
  <TooltipView title="Edit this record" delayDuration={200}>
    <button
      type="button"
      className="inline-flex h-9 items-center gap-1.5 rounded-md border border-border px-3 text-sm font-medium text-foreground hover:bg-accent"
    >
      <Edit className="h-4 w-4" /> Edit
    </button>
  </TooltipView>
  <TooltipView title="Disabled — no permission">
    <span>
      <button
        type="button"
        disabled
        className="inline-flex h-9 items-center justify-center rounded-md border border-border px-3 text-sm font-medium text-foreground opacity-50"
      >
        Save
      </button>
    </span>
  </TooltipView>
</div>