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.
<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`.
—<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.
—<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.
<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.
<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.
<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.
| ID | ROLE | SALARY | |
|---|---|---|---|
| 1 | Ada Lovelace | Engineer | $95,000 |
| 2 | Alan Turing | Researcher | $110,000 |
| 3 | Grace Hopper | Architect | $120,000 |
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.
| # | TEAM | ROLE | CITY | COUNTRY | JOINED | SALARY | STATUS | ACTION | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Ada Lovelace | ada@analytical.engine | Engineering | Principal Engineer | London | United Kingdom | 1843-12-10 | $215,000 | Active | ||
| 2 | Alan Turing | alan@bletchley.uk | Research | Cryptographer | Manchester | United Kingdom | 1936-06-12 | $198,000 | Active | ||
| 3 | Grace Hopper | grace@navy.mil | Compilers | Rear Admiral | Arlington | United States | 1944-07-02 | $184,000 | Active | ||
| 4 | Katherine Johnson | katherine@nasa.gov | Trajectory | Mathematician | Hampton | United States | 1953-06-18 | $172,000 | Active | ||
| 5 | Hedy Lamarr | hedy@spread.spectrum | Comms | Inventor | Vienna | Austria | 1942-08-11 | $165,000 | Pending | ||
| 6 | Tim Berners-Lee | timbl@cern.ch | Web Platform | Director | Geneva | Switzerland | 1989-03-12 | $240,000 | Active | ||
| 7 | Margaret Hamilton | margaret@mit.edu | Apollo | Software Lead | Cambridge | United States | 1965-09-01 | $202,000 | On leave |
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.
| ID | ROLE | SALARY |
|---|
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.
| 1 | Ada Lovelace | Engineer | $95,000 |
| 2 | Alan Turing | Researcher | $110,000 |
| 3 | Grace Hopper | Architect | $120,000 |
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.
<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.
—<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.
default<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.
—<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.
DEFAULT<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.
<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.
<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>