Simple table

Displays rows of data in a card, built from plain JSON columns and rows.

#NameCategoryPriceStock
1iPhone 13Electronics$799.00150
2Galaxy S21Electronics$699.00200
3MacBook ProComputers$1,299.0080
4Dell XPS 13Computers$999.0050
5Sony WH-1000XM4Headphones$349.00120
6AirPods ProHeadphones$249.00180
7Fitbit Charge 5Wearables$179.0075
Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/simple-table.json
Usage
Pass `columns` and `rows` as plain JSON. Every behavior is a prop.
TSImport
import { SimpleTable } from "@/components/ui/simple-table";
TSExample
import { SimpleTable } from "@/components/ui/simple-table";

export function Example() {
  return (
    <SimpleTable
      columns={[
        { key: "name", label: "Name", primary: true },
        { key: "price", label: "Price", format: "currency", align: "right" },
      ]}
      rows={[
        { id: "1", name: "iPhone 13", price: 799 },
        { id: "2", name: "MacBook Pro", price: 1299 },
      ]}
    />
  );
}
Composition
Anatomy of the SimpleTable component.
SimpleTable                       (single component — columns + rows as JSON)
├── card-surface wrapper
│   ├── header                    (only when title / description / action / searchable)
│   │   ├── title + description
│   │   └── search field + action
│   └── horizontal scroller       (scroll-fade-x; maxHeight adds a vertical scroll + sticky header)
│       └── <table>
│           ├── <thead>           (checkbox column? · one <th> per column · actions column?)
│           └── <tbody>           (one <tr> per visible row · nested rows indent under a caret)
│                                 (empty state row when nothing to show)
│
├── columns={[{ key, label?, align?, width?, sortable?, format?, currency?, badges?, primary?, captionKey?, render?, className? }]}
├── rows={[{ id, …fields, children?: [...] }]}
└── props: title · description · action · size · striped · grid · sortable · defaultSort
           searchable · searchPlaceholder · selectable · selectedKeys · defaultSelectedKeys
           onSelectionChange · rowActions · onRowClick · maxHeight · headerCaps · onCellChange
           loading · emptyTitle · emptyDescription
Sorting
`sortable` makes every column sortable; click a header to cycle ascending → descending → off. Seed it with `defaultSort`.
3MacBook ProComputers$1,299.0080
4Dell XPS 13Computers$999.0050
1iPhone 13Electronics$799.00150
2Galaxy S21Electronics$699.00200
5Sony WH-1000XM4Headphones$349.00120
6AirPods ProHeadphones$249.00180
7Fitbit Charge 5Wearables$179.0075
Selection
`selectable` adds a checkbox column with select-all. Control it with `selectedKeys` + `onSelectionChange` for bulk actions.
#NameCategoryPriceStock
1iPhone 13Electronics$799.00150
2Galaxy S21Electronics$699.00200
3MacBook ProComputers$1,299.0080
4Dell XPS 13Computers$999.0050
5Sony WH-1000XM4Headphones$349.00120
6AirPods ProHeadphones$249.00180
7Fitbit Charge 5Wearables$179.0075

Nothing selected.

Row actions
`rowActions` adds a ⋮ menu per row; each action's `onSelect` gets the row. `onRowClick` makes the whole row clickable.
NameEmailRoleStatusJoined
Justice Larkinjustice.larkin@example.comAdminActiveJan 15, 2022
Megan Smithmegan.smith@example.comEditorActiveMar 12, 2022
Daniel Wudaniel.wu@example.comViewerInactiveApr 8, 2022
Sophia Hernandezsophia.hernandez@example.comAdminActiveMay 25, 2022
Liam Johnsonliam.johnson@example.comEditorSuspendedJun 14, 2022
Emily Brownemily.brown@example.comViewerActiveJul 9, 2022
Expandable rows
Give a row a `children` array and it gets an expand caret. Search keeps a parent visible when a child matches and opens it.
Projects
ProjectOwnerStatusProgress
Website redesign
Emma TaylorIn progress72%
Mobile app onboarding
Sophia BrownBlocked39%
Security audit
Charlotte AllenPlanned12%
Striped, grid and dense
`striped` swaps dividers for zebra rows, `grid` adds column dividers, `size="sm"` tightens rows.
#NameCategoryPriceStock
1iPhone 13Electronics$799.00150
2Galaxy S21Electronics$699.00200
3MacBook ProComputers$1,299.0080
4Dell XPS 13Computers$999.0050
5Sony WH-1000XM4Headphones$349.00120
6AirPods ProHeadphones$249.00180
7Fitbit Charge 5Wearables$179.0075
Thumbnails
`format: "image"` draws a URL as a thumbnail. `captionKey` adds a muted second line under a cell.
NameUpdated
https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=160&q=80
Easter seriesSermon graphic
Apr 2, 2026
https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=160&q=80
Fall retreatEvent flyer
Sep 18, 2026
https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=160&q=80
Kids campSocial post
Jun 11, 2026
Checkbox columns
`format: "checkbox"` turns boolean cells into toggles — `onCellChange` reports each tick. A string value draws a thumbnail in place of the box. `headerCaps` gives the small uppercase header.
SizeDesignBkg
Screen1920 × 1080 px
Screen
IG Feed1080 × 1350 px
Story1080 × 1920 px
Square1080 × 1080 px
Portrait1200 × 1800 px
Letter8.5 × 11 in
Loading
`loading` shows skeleton rows shaped like each column while the table has no rows (`true` is 5 rows; a number sets the count). A response inside 300ms never flashes one, a skeleton that appears stays at least 500ms, and the rows then replace it with a cross-fade and blur. With rows already on screen `loading` changes nothing, and the empty message waits until loading is done. Pick a latency and a result, then Reload.
Loading Products…
Products
#NameCategoryPriceStock
Empty state
With no rows (or no search matches) the body shows `emptyTitle` + `emptyDescription`.
#NameCategoryPriceStock
No products yet
Add a product and it will show up here.
API Reference
Props exposed by the SimpleTable component.

SimpleTable

PropTypeDefaultDescription
columnsColumn[]—Column definitions — see Column below.
rowsRecord<string, unknown>[]—Plain JSON rows. Each field is read by the column with the matching `key`.
rowKeystring"id"Row field used as the React key and selection id. Falls back to the row index.
childrenKeystring"children"Row field holding nested rows. Rows with children get an expand caret on the first column.
titleReactNode—Card header title.
descriptionReactNode—Muted line under the title.
actionReactNode—Right side of the header row (a Button, say).
size"sm" | "md""md"`sm` tightens row padding for dense data.
stripedbooleanfalseZebra rows instead of row dividers.
gridbooleanfalseVertical dividers between columns.
sortablebooleanfalseMakes every column sortable. A column's own `sortable: false` opts out. Clicking cycles asc → desc → off.
defaultSort{ key: string; direction: "asc" | "desc" }—Initial sort.
searchablebooleanfalseAdds a search field to the header. Filters rows (a parent stays when a child matches) and highlights matches.
searchPlaceholderstring"Search"Placeholder + accessible label for the search field.
selectablebooleanfalseLeading checkbox column with a select-all header (selects every visible row).
selectedKeysstring[]—Controlled selection. Nested row keys are `parent/child`.
defaultSelectedKeysstring[]—Uncontrolled initial selection.
onSelectionChange(keys: string[]) => void—Fires with the full selected key list.
rowActionsAction[]—Trailing ⋮ menu on each row — see Action below.
onRowClick(row) => void—Makes rows clickable (pointer + hover tint).
maxHeightnumber | string—Caps the table height; rows scroll under a sticky header.
headerCapsbooleanfalseSmall uppercase header labels.
onCellChange(row, key: string, value: boolean) => void—Fires when a `checkbox` cell is toggled. Cells keep their own state, so it's optional.
loadingboolean | number—First-load state. While true AND `rows` is empty, the body shows skeleton rows shaped like each column — a pill for a badge, a short bar for a number or date, a square for an image or checkbox (`true` = 5 rows; a number sets the count). A response faster than 300ms never shows a skeleton; one that appears stays at least 500ms, then the rows replace it with a cross-fade and blur. With rows already on screen it changes nothing, so a refetch keeps the stale rows readable. The empty message never shows while loading; the select-all checkbox is disabled; search stays usable.
emptyTitlestring"No data found"Shown when there are no rows (or no search matches).
emptyDescriptionstring"No information is currently available in this section."Line under the empty title.
classNamestring—Class names on the card wrapper.

Column

PropTypeDefaultDescription
keystring—Row field this column reads.
labelstring—Header text. Defaults to `key`.
align"left" | "center" | "right""left"Text alignment for header and cells.
widthnumber | string—Fixed column width (px or any CSS length).
sortableboolean—Sort on this column (overrides the table-level `sortable`).
format"text" | "number" | "currency" | "percent" | "date" | "badge" | "image" | "checkbox""text"How the value renders. Number formats use tabular figures; `percent` expects a 0–1 fraction; `date` parses any `Date`-readable string; `image` draws a URL as a thumbnail; `checkbox` draws a boolean as a toggle (a string value draws a thumbnail instead).
currencystring"USD"ISO currency code for `format: "currency"`.
badgesRecord<string, BadgeColor>—Value → Badge color map for `format: "badge"`. Unmapped values render muted.
primaryboolean—Medium-weight foreground text — use on the row's name column.
captionKeystring—Row field rendered as a small muted second line under the cell value.
render(value, row) => ReactNode—Escape hatch for cells JSON can't express. Rendered cells are skipped by search.
classNamestring—Class names on each body cell.

Action

PropTypeDefaultDescription
"separator"string—A divider between menu items.
labelstring—Menu item text.
iconReactNode—Leading icon.
destructiveboolean—Tints the item red.
onSelect(row) => void—Called with the row the menu belongs to.