Tables & lists
Records to scan and act on: grids, tables, logs, grouped and bulk tables, spreadsheets and lists.
Installation & usage
npx @youj/cli add tables-data-gridAlso installs avatar, badge, button, checkbox, collapse, command, content-swap, data-table, dropdown-menu, icon-archive, icon-arrow-down, icon-arrow-up, icon-arrow-up-right, icon-check, icon-check-circle, icon-chevron-left, icon-chevron-right, icon-circle-alert, icon-columns, icon-copy, icon-external-link, icon-filter, icon-inbox, icon-layers, icon-link, icon-minus, icon-more-horizontal, icon-rotate-ccw, icon-rotate-cw, icon-search, icon-search-x, icon-sort, icon-trending-down, icon-trending-up, icon-x, input-group, kbd, pagination, popover, presence-list, segmented-control, skeleton, sliding-indicator, sortable-list, switch, tabs, tooltip, use-announcer, use-now, youj-core, youj-labels, youj-presets, youj-ui-core.
The CLI copies the block to blocks/tables/data-grid.tsx with every component it uses, so it’s yours to edit.
Every prop is optional and falls back to the demo, so it renders as you see it until you pass your own. Give it your data, rewrite or translate its strings through copy, and connect its actions:
'use client'
import { DataGrid, type DataGridProps } from '@/blocks/tables/data-grid'
export function DataGridView({ rowActions, density }: Pick<DataGridProps, 'rowActions' | 'density'>) {
return (
<DataGrid
rowActions={rowActions}
density={density}
onSortingChange={() => {
// your code
}}
onColumnFiltersChange={() => {
// your code
}}
onGlobalFilterChange={() => {
// your code
}}
/>
)
}Description
The records engine: a rich cell for every kind of value (an identity with its logo, a status, tags, a meter, a trend line, money, relative dates, people, a switch, a copyable id), row actions where the pointer is, grouping with totals, detail panels, saved views as tabs and cards on a narrow screen. Describe each column by its kind and the grid draws it.
The engine the others are built on. Give it your columns and rows and it handles the rest: sorting that glides rows into place, a filter for each column you declare (values with counts, ranges, dates) shown as chips, search, saved views, header menus to sort, pin, move and hide, resizing, density, CSV export, row selection with an action bar, inline editing that saves optimistically and rolls back, server mode and virtualization for thousands of rows. Arrow keys move between cells, as in a spreadsheet.
People you work through, shown as a sales pipeline: each record's organization, stage, amount, owner, tags and last touch, in views you define (everyone, yours, hot leads, customers). Move a record to another stage from its own row, open it for its details, or select a few to email, assign, tag, export or delete them. The stories also show job candidates and patient follow-ups.
Rows worked in bulk, shown as a store's orders. Select some and the selection bar becomes a command bar: the actions you define (here fulfill, print labels, mark as paid, refund after a confirmation that names the count, archive or export), each with its own key and all of them in a menu you can search. The statuses, channels and actions are your data. Every action shows at once and goes back, with the reason, if your app refuses it. The stories also show expense reports and library holds.
Rows that work like a spreadsheet, shown as a stock sheet. Edit any cell in place, and read the active cell's address and contents in the name box and formula bar (Value shows its formula). Paste a block of cells from Excel or Google Sheets and it lands from the active cell on, each value checked against its column. ⌘Z and ⇧⌘Z undo and redo, a paste as one step, and the totals row stays in view as you scroll.
Planned against spent as a tree, shown as a budget. Lines gather the ways you define (the demo: by team inside their department, by category or by owner), and every group sums its plan, its spend, how far over or under it runs and how much it has used, down to a grand total. Open a group and its lines slide in under it; sort a money column and every level sorts inside itself.
A live event stream, virtualized for thousands of rows. Read from the top and it follows new events as they glow in; scroll down or pause and it holds still, counting what's waiting in a pill that takes you back up. Arrow through the events and the inspector beside them shows each one in full: request, duration and its payload as JSON to copy.
See who did what with a searchable, sortable audit log; a row opens the event in a sheet. How events end and their kinds are your data (the stories show a workspace, a clinic's records access and a shared drive). Embedded, it sits as a section inside a settings page.
Recent events with a duration as a table to filter by cause and sort, with a last-24-hours strip per source: a mark for every entry in its cause's color and a line at now. Tick entries and Assign cause files them all under one cause. Shown with a plant's downtime; stories also use support escalations and clinic delays. format sets how durations read and longAfter where an entry counts as long.
Things with a picture, listed with search, sort and a create button above: a picture of each, its name and status, where to find it, who made it and when, and its latest activity, with Details as the one primary button and Visit, Activity and Settings as icon buttons with tooltips. The statuses and the pictures it draws are your data; the stories show workspace projects, real estate listings and a course catalog.
A panel for transfers in flight: overall progress, rate and time left, then a row per item with pause, resume and cancel. Failed items say why and offer Retry, and finished ones sink to the foot. format writes sizes and rates in your own unit, so it counts rows for a table migration or frames for video exports as well as bytes for a file sync, and loading, error and empty take the rows' place at the card's size.
Code
Data Grid, the Data grid story: the files youj add writes.
blocks/tables
Reference
Props65 props, 11 types
Props
| Prop | Type | |
|---|---|---|
rows | ReadonlyArray<TData>The records, one per row. Default: 18 projects, with the demo's columns. | – |
columns | ReadonlyArray<DataGridColumnDef<TData>>The columns, from | – |
getRowId | (row: TData, index: number) => stringStable row identity: selection, expansion and row motion key on it. Default: its | – |
getRowLabel | (row: TData) => stringNames a row for its checkbox, its menus and announcements. Default: its first column's value. | – |
title | ReactNodeThe heading over the grid. Without one the grid stands alone, named by | – |
description | ReactNodeA line under the title. | – |
headingLevel | 2 | 3 | 4The title's level when default 2 | 2 |
label | stringThe grid's accessible name, when it has no | – |
aria-labelledby | stringIdentifies the element (or elements) that labels the current element. | – |
initialState | Partial<DataGridState>Starting state for every slice you don't control. | – |
state | Partial<DataGridState>Controlled slices. Pair each with its change callback. | – |
onSortingChange | Change<SortingState> | – |
onColumnFiltersChange | Change<ColumnFiltersState> | – |
onGlobalFilterChange | Change<string> | – |
onRowSelectionChange | Change<RowSelectionState> | – |
onColumnVisibilityChange | Change<ColumnVisibilityState> | – |
onColumnOrderChange | Change<ColumnOrderState> | – |
onColumnSizingChange | Change<ColumnSizingState> | – |
onGroupingChange | Change<string | null> | – |
onExpandedChange | Change<Record<string, boolean>> | – |
onPageIndexChange | Change<number> | – |
selectable | boolean | ((row: TData) => boolean)A checkbox on every row, or a function deciding per row. | – |
bulkActions | (selection: DataGridSelection<TData>) => ReactNodeThe selection bar's actions. | – |
rowActions | (row: TData) => DataGridRowAction<TData>[]Each row's actions: | – |
onRowOpen | (row: TData) => voidEnter on a row (or a click on its name) opens it. | – |
onRowClick | (row: TData) => voidA click on a row (or card), anywhere but its own controls (checkbox, actions, links, detail panel) and not while text is selected. Keyboard opens go through | – |
renderDetail | (row: TData) => ReactNodeA detail panel under a row, opened from its chevron (or Enter, when rows don't open). | – |
activeRowId | string | nullThe row open elsewhere (in a side panel or a drawer): it keeps the selection's tint and bar, and | – |
density | DataGridDensity'compact''comfortable''spacious' | – |
defaultDensity | DataGridDensity'compact''comfortable''spacious'Default: | – |
onDensityChange | Change<DataGridDensity> | – |
striped | booleanAlternate rows take a faint tint, instead of hairlines alone. | – |
enableMultiSort | booleanShift+click on a header adds it to the sort. default true | true |
enableColumnResizing | booleanDrag a header's edge to resize its column. default true | true |
pinFirstColumn | booleanPins the first column (the identity) while the grid scrolls sideways. default true | true |
search | boolean | { placeholder?: string; shortcut?: string | false }A search over every visible column. default true | true |
filters | booleanThe Filter menu, for columns that filter (status, tags, people…). default true | true |
grouping | booleanThe Group menu, for columns that group. default true | true |
columnsMenu | booleanThe Columns menu: show, hide and reorder. default true | true |
densitySwitch | booleanThe density switch. default true | true |
views | ReadonlyArray<DataGridView>Saved views, as tabs over the toolbar. | – |
activeView | string | – |
defaultActiveView | string | – |
onActiveViewChange | Change<string> | – |
pageSize | number | falseRows per page, or false for every row. Grouped rows aren't paged: groups fold instead. default 25 | 25 |
virtualize | booleanRenders only the rows in view, for thousands of rows. Needs a default false | false |
height | number | stringThe scroll area's height: the header sticks and virtualized rows scroll inside it. | – |
maxHeight | number | string | – |
onEndReached | () => voidCalled when a virtualized grid scrolls to its end: load the next rows. | – |
loadingMore | booleanRows are on their way (with | – |
loading | booleanNo rows yet: skeleton rows that resolve into the data. With rows: a progress line, and nothing moves. default false | false |
skeletonRows | number | – |
error | ReactNodeReplaces the rows with an error panel. A string is its message. | – |
onRetry | () => void | – |
empty | DataGridEmptyThe empty state: its title, description, an illustration or icon, and an action. | – |
layout | 'auto' | 'table' | 'cards'
| – |
cardsBelow | numberThe width (px) under which default 640 | 640 |
toolbar | ReactNodeThe start of the toolbar: a scope switch, a count. | – |
actions | ReactNodeThe end of the toolbar: "New customer". | – |
now | number | DateThe moment relative dates count from. Default: the browser's clock, each minute. | – |
theme | ThemeInputWraps the grid in this theme. Default: the theme around it. | – |
embedded | booleanInside another page: the grid alone, at its natural width (72rem at 100% scale) unless default true | true |
fill | booleanTake the whole width it's given instead of stopping at its natural width. default false | false |
copy | Partial<DataGridCopy>Any of the grid's strings, to rewrite or translate. | – |
locale | stringFormats its numbers and dates (BCP 47). Default: the app's locale (YoujProvider). | – |
DataGridState
| Prop | Type | |
|---|---|---|
sortingRequired | SortingState | – |
columnFiltersRequired | ColumnFiltersState | – |
globalFilterRequired | string | – |
rowSelectionRequired | RowSelectionState | – |
columnVisibilityRequired | ColumnVisibilityState | – |
columnOrderRequired | ColumnOrderState | – |
columnSizingRequired | ColumnSizingState | – |
columnPinningRequired | ColumnPinningState | – |
groupingRequired | string | nullThe column rows are grouped by, or null. | – |
expandedRequired | Record<string, boolean>Rows whose detail panel is open. | – |
collapsedGroupsRequired | Record<string, boolean>Groups that are folded shut, by key. | – |
pageIndexRequired | number | – |
pageSizeRequired | number | – |
DataGridView
| Prop | Type | |
|---|---|---|
idRequired | string | – |
labelRequired | string | – |
stateRequired | Partial<Pick<DataGridState, 'sorting' | 'columnFilters' | 'globalFilter' | 'grouping' | 'columnVisibility'>>What the view shows. Slices it leaves out go back to their defaults. | – |
count | numberShown after its name, e.g. how many rows it holds. | – |
DataGridSelection
| Prop | Type | |
|---|---|---|
countRequired | numberHow many rows are selected. | – |
rowsRequired | TData[]The selected rows, in view order. | – |
idsRequired | string[] | – |
clearRequired | () => void | – |
Project
| Prop | Type | |
|---|---|---|
idRequired | string | – |
nameRequired | string | – |
teamRequired | 'Platform' | 'Growth' | 'Mobile' | 'Design systems' | 'Security' | – |
statusRequired | ProjectStatus'on-track''at-risk''off-track''paused''done' | – |
membersRequired | DataGridPerson[] | – |
doneRequired | number | – |
tasksRequired | number | – |
velocityRequired | number[]Story points shipped each week, the last ten weeks. | – |
budgetRequired | number | – |
targetRequired | string | – |
tagsRequired | string[] | – |
summaryRequired | string | – |
DataGridColumnOptions
| Prop | Type | |
|---|---|---|
id | stringRequired when the accessor is a function. | – |
header | string | (() => ReactNode) | – |
size | number | – |
minSize | number | – |
maxSize | number | – |
enableSorting | boolean | – |
enableHiding | boolean | – |
enableResizing | boolean | – |
render | (value: TValue, row: TData) => ReactNodeDraws the cell yourself. The type's sorting, search, filter and grouping stay. | – |
meta | Partial<DataGridColumnMeta>Anything the type sets, changed. | – |
DataGridRowAction
| Prop | Type | |
|---|---|---|
labelRequired | string | – |
icon | ComponentType<{ className?: string; size?: number }> | – |
onSelectRequired | (row: TData) => void | – |
inline | booleanAlso an icon button on the row, shown on hover and focus. | – |
destructive | boolean | – |
separated | booleanA line above it in the menu. | – |
disabled | boolean | – |
shortcut | stringShown in the menu beside it, e.g. "⌘E". | – |
DataGridEmpty
| Prop | Type | |
|---|---|---|
title | string | – |
description | ReactNode | – |
illustration | ReactNodeAn illustration or an icon above the words. Default: an inbox. | – |
action | ReactNodeThe next step: "Add a customer". | – |
DataGridColumnMeta
| Prop | Type | |
|---|---|---|
kind | DataGridColumnKind'identity''status''tags''meter''trend''currency''number''date''people''boolean''id''link''text'What the column holds; the column types set it. | – |
label | stringThe column's name in words: menus, cards, announcements. Default: a string header, then the id. | – |
align | 'start' | 'center' | 'end'Cell and header alignment. Figures default to | – |
numeric | booleanFigures: tabular mono, end-aligned. | – |
grow | booleanTakes the width the other columns leave (never narrower than its size). | – |
facet | DataGridFacetA filter in the toolbar's Filter menu. | – |
groupable | booleanRows can be grouped by this column. | – |
groupKey | (value: unknown) => stringThe group a cell value falls in. Default: the value as a string. | – |
groupLabel | (key: string) => ReactNodeA group's heading. Default: the key. | – |
groupOrder | (a: string, b: string) => numberOrders the groups. Default: the options' order, else A to Z. | – |
aggregate | 'sum' | 'mean' | ((values: unknown[]) => ReactNode)A figure for each group's footer: a sum, a mean, or your own. | – |
formatAggregate | (value: number) => ReactNodeFormats a | – |
skeleton | DataGridSkeleton'text''identity''badge''tags''number''meter''trend''people''toggle''none'The placeholder shape while the grid loads. | – |
card | 'title' | 'badge' | 'field' | 'hidden'Its place on a card, when the grid lays out as cards: the title, a badge beside it, a field, or left off. | – |
searchText | (value: unknown) => stringWhat the search matches for this column. Default: the value as text. | – |
cellClassName | stringClasses for this column's body cells. | – |
headerClassName | stringClasses for this column's header cell. | – |
DataGridFacet
| Prop | Type | |
|---|---|---|
title | stringThe filter's name in the menu and on its chip. Default: the column's label. | – |
options | readonly DataGridOption[]The values offered, in order. Default: every value found in the rows. | – |
keysRequired | (value: unknown) => string[]The values a cell holds: one for a status, several for tags or people. | – |
describe | (key: string, value: unknown) => DataGridOption | undefinedNames a value that isn't in | – |
DataGridOption
| Prop | Type | |
|---|---|---|
valueRequired | string | – |
labelRequired | string | – |
tone | DataGridOptionTone'neutral''accent''success''warning''danger''chart-1''chart-2''chart-3''chart-4''chart-5' | – |
src | stringAn image for the option (a person's avatar). | – |
DataGridPerson
| Prop | Type | |
|---|---|---|
idRequired | string | – |
nameRequired | string | – |
src | string | – |
Strings76 keys for copy
Strings
copy takes any of these keys; the rest keep their defaults. A key whose text holds a value (a count, a name) takes a function.
searchjustNowactionsclearFiltersviewscopiedcopyFailedsearchPlaceholderselectRowselectedcolumnsnoItemstryAgainsortByclearSelectionclearSearchclearAllactiveFiltersremoveFilterrowitemCountloadFailedpaginationnoMatchesemptyHintescKeytrendUptrendDowntrendFlatrowNumbertoggleColumnsresetColumnssortAscendingsortDescendingrowActionsrowDensitycompactRowscomfortableRowsfilterByfilterNoOptionsclearFilterpreviousPagenextPagerangeOfpageOfselectionActionsselectAllCountselectAllRowsselectAllOnPagesortedBysortedColumnsortClearedselectionClearedselectedRowsmatchCounttotalCountcellOfgridHintArrowsgridHintSelectgridHintSortnoMatchesHintspaciousRowsgroupBynoGroupinggroupedBygroupingClearedshowDetailshideDetailscopyValueaddFilterbooleanYesbooleanNovalueOfMaxgridHintOpengroupTotalnounBuilt from21 components
Building your ownthe engines, the kit, server data
Building your own
Contacts, Orders and Inventory are the Data table with columns, copy and a header of their own, and Logs is the Data table virtualized beside an inspector: read them to see how a domain table is put together. For a table of your own, start from the Data table and pass rows and columns (made with createDataTableColumnHelper from @youj/react/data-table). For data that lives on a server, pass manual with one page of rows and the rowCount, and own the sorting, filters and pages through state and their on…Change callbacks.
The parts these tables are made of are components: the Table primitives and the Data table kit (column header, toolbar, search, filters and chips, view options, pagination, selection column and bar, saved views, the cell editor, the keyboard grid and CSV export), from @youj/react/data-table. Use them when a table needs a layout the engines don't have.
Accessibilitygrid and treegrid roles, keyboard, announcements, motion
Accessibility
The Data table and the tables built on it are a native <table role="grid"> named by their title, and a single Tab stop: the arrow keys move between cells, Home and End to a row's ends, Ctrl+Home and Ctrl+End to the corners, Page Up and Page Down a screen at a time. Space selects a row (Shift+Space a range), Ctrl+A or ⌘A every row and Escape clears the selection; Enter, F2 or typing edits a cell, Alt+↓ opens a header's menu and Shift+←/→ resizes a column. The keys are listed in the grid's description. aria-rowcount and aria-rowindex keep every row's place when the table is paged or virtualized, headers carry aria-sort, and selection uses aria-multiselectable and aria-selected. Saves, rollbacks, sorting and selection are announced in a live region.
In Orders the command bar is a group named "Selection actions"; each action's key is in its aria-keyshortcuts, and a refund asks first in an alert dialog that names the count. In Inventory the name box is an output named "Cell", the formula bar is named for the cell it edits ("Contents of E7"), a value that doesn't fit is described on the field with aria-invalid, and undo, redo, copy and paste are announced. Low stock is said in words, not only in amber. The stories show other worlds, with their own names and words.
Budget is a treegrid: every row carries aria-level, aria-posinset and aria-setsize, groups aria-expanded. Up and Down move between rows, Right opens a group (or moves into it), Left closes it (or moves to its parent), mirrored in right-to-left text, and Enter opens a line. Variance is read as over or under budget, and opening or closing a group is announced. The stories show other worlds, with their own names and words.
Logs keeps the inspector in a complementary region named "Event". The pill for waiting events is a button, and a polite status says once that events are waiting rather than counting each one. Levels are written out, so color is never the only signal.
Cause log lists the entries in a real table with a tick per row and one for all; every tick is named for its source and the time. Each source's strip is an image whose text gives its entries and total in the last 24 hours, and Assign cause is a menu that stays disabled until a stop is ticked and announces the result.
Preview list is a labelled list of projects; each row names its status in words (the dot only echoes it), its picture is decorative, and the icon buttons carry the project's name in their labels and tooltips. Search and sort are named controls, and the list scrolls inside the card so a search never resizes it.
Transfer list groups the overall meter under a label, and every row is a named progress bar with pause, resume, cancel and retry buttons named for the item. While it loads the skeletons are one named status.
Under reduced motion rows change in place instead of gliding, the waiting-events pill appears without moving, the live dot stops pulsing and groups open at once.