Yugen UI

DataGrid

DataGrid is an interactive data grid: sorting, row selection, resizable columns and virtualization for tens of thousands of rows.

The equivalent of Fluent 2's DataGrid. Define columns with createColumn<T>() (label, renderCell, compare, width…) and pass items + getRowId. The grid has role="grid" with correct aria-rowcount / aria-rowindex even when virtualized, and a single tab stop (roving tabindex): the arrow keys move cell by cell (left / right swap in RTL), plus Home / End, Ctrl + Home / End and PageUp / PageDown. Space selects a row; Shift + arrow or Shift + click selects a range. People choose which columns to show, and their order, with DataGridColumnPicker (hiddenColumns, columnOrder). The header is sticky. loading shows a skeleton following Wait UX (useSpinDelay), and a grid with no rows shows Empty. The logic is reusable through useDataGridSort, useDataGridSelection, useDataGridColumnVisibility and the pure functions sortItems, getVisibleColumns and getOrderedColumns.
Customer
Order ID
Status
Date(descending)
Amount
Hana Sato
ORD-10012
Failed
Dec 10, 2026
$345.00
Alex Chen
ORD-10011
Paid
Nov 27, 2026
$2,620.00
Priya Patel
ORD-10010
Paid
Oct 16, 2026
$4,895.00
Chris Novak
ORD-10009
Paid
Sep 5, 2026
$865.00
Ines Duarte
ORD-10008
Pending
Aug 22, 2026
$3,140.00
Priya Patel
ORD-10020
Paid
Aug 14, 2026
$1,060.00
Sam Lee
ORD-10007
Failed
Jul 11, 2026
$5,415.00
Chris Novak
ORD-10019
Paid
Jul 3, 2026
$3,335.00
Taylor Nguyen
ORD-10006
Paid
Jun 28, 2026
$1,385.00
Ines Duarte
ORD-10018
Pending
Jun 20, 2026
$5,610.00
Ben Carter
ORD-10005
Paid
May 17, 2026
$3,660.00
Sam Lee
ORD-10017
Failed
May 9, 2026
$1,580.00
Taylor Nguyen
ORD-10016
Paid
Apr 26, 2026
$3,855.00
Maria Garcia
ORD-10004
Paid
Apr 6, 2026
$5,935.00
Jordan Kim
ORD-10003
Pending
Mar 23, 2026
$1,905.00
Ben Carter
ORD-10015
Paid
Mar 15, 2026
$6,130.00
Hana Sato
ORD-10002
Failed
Feb 12, 2026
$4,180.00
Maria Garcia
ORD-10014
Paid
Feb 4, 2026
$2,100.00
Jordan Kim
ORD-10013
Pending
Jan 21, 2026
$4,375.00
Alex Chen
ORD-10001
Paid
Jan 1, 2026
$150.00

Import

Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.

TSX
// npm install yugen-uiimport {  DataGrid, DataGridColumnPicker, createColumn, sortItems, getVisibleColumns, getOrderedColumns,  useDataGridSort, useDataGridSelection, useDataGridColumnVisibility,  type DataGridColumn, type DataGridSortState,} from "yugen-ui";

Variants

Each tile is a standalone example: hover to copy, or press Code to see the source.

Sorted by: amount · descending
Customer
Order ID
Status
Date
Amount(descending)
Maria Garcia
ORD-10004
Paid
Apr 6, 2026
$5,935.00
Hana Sato
ORD-10002
Failed
Feb 12, 2026
$4,180.00
Ben Carter
ORD-10005
Paid
May 17, 2026
$3,660.00
Jordan Kim
ORD-10003
Pending
Mar 23, 2026
$1,905.00
Taylor Nguyen
ORD-10006
Paid
Jun 28, 2026
$1,385.00
Alex Chen
ORD-10001
Paid
Jan 1, 2026
$150.00

Sorting (controlled)

Only columns with compare can be sorted. Sorting is stable, aria-sort is set on the header, and the icon comes with text for screen readers.

Customer
Order ID
Status
Alex Chen
ORD-10001
Paid
Hana Sato
ORD-10002
Failed
Jordan Kim
ORD-10003
Pending
Maria Garcia
ORD-10004
Paid
Ben Carter
ORD-10005
Paid
Taylor Nguyen
ORD-10006
Paid
Sam Lee
ORD-10007
Failed
Ines Duarte
ORD-10008
Pending

Multiple selection + toolbar

selectedItems / onSelectionChange is a Set<string> of ids. The header checkbox has an indeterminate state.

Customer
Status
Date(descending)
Amount
Taylor Nguyen
Paid
Jun 28, 2026
$1,385.00
Ben Carter
Paid
May 17, 2026
$3,660.00
Maria Garcia
Paid
Apr 6, 2026
$5,935.00
Jordan Kim
Pending
Mar 23, 2026
$1,905.00
Hana Sato
Failed
Feb 12, 2026
$4,180.00
Alex Chen
Paid
Jan 1, 2026
$150.00

Choosing and ordering columns

DataGridColumnPicker and DataGrid share hiddenColumns and columnOrder, both saved to localStorage. The menu stays open while you check columns; the hideable: false column (Customer) and the last visible column can't be unchecked. Drag an item in the menu (or press Alt + ↑ ↓) to change the column order; the grid follows right away. Hiding the sorted column keeps the row order, and the menu still shows the arrow on that column.

Drag the right edge of a header, or Tab to the handle and use the arrow keys.
Select row
Customer
Order ID
Status
Date
Amount
Alex Chen
ORD-10001
Paid
Jan 1, 2026
$150.00
Hana Sato
ORD-10002
Failed
Feb 12, 2026
$4,180.00
Jordan Kim
ORD-10003
Pending
Mar 23, 2026
$1,905.00
Maria Garcia
ORD-10004
Paid
Apr 6, 2026
$5,935.00
Ben Carter
ORD-10005
Paid
May 17, 2026
$3,660.00

Column resizing, pinned first column

The handle receives focus when you Tab from a header cell: ← / → ±10px, Shift ±50px, double-click to fit the content. The sticky column stays in place when scrolling horizontally.

Customer
Order ID
Status
Date
Amount
Alex Chen
ORD-10001
Paid
Jan 1, 2026
$150.00
Hana Sato
ORD-10002
Failed
Feb 12, 2026
$4,180.00
Jordan Kim
ORD-10003
Pending
Mar 23, 2026
$1,905.00
Maria Garcia
ORD-10004
Paid
Apr 6, 2026
$5,935.00
Ben Carter
ORD-10005
Paid
May 17, 2026
$3,660.00
Taylor Nguyen
ORD-10006
Paid
Jun 28, 2026
$1,385.00
Sam Lee
ORD-10007
Failed
Jul 11, 2026
$5,415.00
Ines Duarte
ORD-10008
Pending
Aug 22, 2026
$3,140.00
Chris Novak
ORD-10009
Paid
Sep 5, 2026
$865.00
Priya Patel
ORD-10010
Paid
Oct 16, 2026
$4,895.00
Alex Chen
ORD-10011
Paid
Nov 27, 2026
$2,620.00
Hana Sato
ORD-10012
Failed
Dec 10, 2026
$345.00
Jordan Kim
ORD-10013
Pending
Jan 21, 2026
$4,375.00
Maria Garcia
ORD-10014
Paid
Feb 4, 2026
$2,100.00
Ben Carter
ORD-10015
Paid
Mar 15, 2026
$6,130.00
Taylor Nguyen
ORD-10016
Paid
Apr 26, 2026
$3,855.00
Sam Lee
ORD-10017
Failed
May 9, 2026
$1,580.00

Virtualizing 10,000 rows

virtualized + height: only rows in the viewport (+ overscan) are rendered. Ctrl + End scrolls to the last row and focuses it.

Customer
Order ID
Status
Alex Chen
ORD-10001
Paid
Hana Sato
ORD-10002
Failed
Jordan Kim
ORD-10003
Pending
Maria Garcia
ORD-10004
Paid

Loading · empty · error

loading shows a skeleton after 300ms (kept for at least 500ms); no rows shows Empty; errors are handled by AsyncContent, with a Retry button.

المدينة
الدولة
السكان (ألف)
القاهرة
مصر
22,183
الرياض
السعودية
7,682
دبي
الإمارات
3,604

RTL

Columns run right to left, ArrowLeft moves toward the end of the row, and dragging the handle left widens the column. No extra prop: the direction comes from DirectionProvider / YugenProvider.

Usage

Do
  • Set aria-label (or aria-labelledby) on the grid, and make getRowId return a stable id, not the array index.
  • Turn on virtualized for lists longer than a few hundred rows, with a fixed height and a uniform rowHeight.
  • Use align="end" for number and currency columns and a fixed width for narrow columns; let the main column flex.
Don't
  • Don't put several buttons in each cell, since each one is a tab stop. Group them in an actions Menu.
  • Don't use DataGrid for layout or card lists. Use Table (static) or List.
  • Don't show your own spinner when reloading. Use the loading prop so the loader doesn't flash for less than 300ms.

Props

Prop · TypeDefaultDescription
itemsreadonly T[]Default: —Data in its original order; T is inferred for columns and getRowId.
columnsDataGridColumn<T>[]Default: —Created with createColumn<T>({ columnId, label?, renderHeaderCell?, renderCell, compare?, width?, minWidth?, maxWidth?, resizable?, align?, sticky?, hideable? }). label is the plain-text header and the name in DataGridColumnPicker; use renderHeaderCell when the header needs more than text.
columnOrderstring[]Default: —Display order of the columns (column ids). Columns missing from the array come after, in columns order; unknown ids are ignored.
hiddenColumnsReadonlySet<string>Default: —Ids of the columns not shown. A hideable: false column is always shown; hiding every column keeps the first one. A hidden column keeps its resized width and still sorts the rows if it is the sort column; focus moves to the next column when the focused one is hidden.
getRowId(item: T) => stringDefault: —Stable row id (key, selection).
size"small" | "medium" | "large"Default: "medium"Row height 34 / 44 / 56px.
sortablebooleanDefault: falseClick / Enter / Space on the header of a column with compare sorts by it (the same column again reverses the direction).
sortState / defaultSortState{ sortColumn: string | undefined; sortDirection: "ascending" | "descending" }Default: —Sort state (controlled / initial).
onSortChange(event, state) => voidDefault: —Called when the sort column or direction changes.
selectionMode"single" | "multiselect"Default: —Adds a selection column. Without it, rows can't be selected.
selectedItems / defaultSelectedItemsReadonlySet<string> / Iterable<string>Default: —Selected ids (controlled / initial).
onSelectionChange(event, { selectedItems: Set<string> }) => voidDefault: —Called when the selection changes (click, Space, Shift + arrow, select all, Ctrl + A).
selectionAppearance"brand" | "neutral" | "none"Default: "brand"Background of selected rows.
subtleSelectionbooleanDefault: falseCheckboxes only show on hover / focus or when the row is selected.
resizableColumnsbooleanDefault: falseResize handle on the trailing edge of each header (mouse, touch, keyboard; double-click fits the content).
columnSizingOptionsRecord<columnId, { defaultWidth?, minWidth?, maxWidth? }>Default: —Per-column size overrides (as in Fluent).
onColumnResize(event, { columnId, width }) => voidDefault: —Called when a column width changes. Nothing is saved by default.
virtualizedbooleanDefault: falseRenders only the rows in the viewport; aria-rowcount / aria-rowindex still reflect the real total.
rowHeightnumberDefault: —Row height (px); defaults to the value for size.
heightnumber | stringDefault: —Height of the scroll container. virtualized without it → 400px (h-100).
overscannumberDefault: 6Extra rows rendered above / below the viewport.
focusMode"cell" | "row"Default: "cell"Arrow keys move by cell or by row.
loadingbooleanDefault: falseSkeleton after 300ms, kept for at least 500ms (useSpinDelay); aria-busy on the grid.
emptyReactNodeDefault: —Content when there are no rows; defaults to an Empty reading “No data”.
DataGridColumnPicker.columnsDataGridColumn<T>[] | { columnId, label?, hideable? }[]Default: —The same column array passed to DataGrid; for Table, an array of { columnId, label }. The menu shows label.
DataGridColumnPicker.hiddenColumnsReadonlySet<string>Default: —Ids of the hidden columns, the same value passed to DataGrid.
DataGridColumnPicker.onHiddenColumnsChange(event, { hiddenColumns: Set<string> }) => voidDefault: —Called when a column is checked / unchecked or Show all columns is chosen. Ids of columns that are not in columns right now are kept, so they can be saved.
DataGridColumnPicker.columnOrderstring[]Default: —Column order, the same value passed to DataGrid.
DataGridColumnPicker.onColumnOrderChange(event, { columnOrder: string[] }) => voidDefault: —When set, menu items can be dragged (or moved with Alt + ↑ ↓) to change the column order. Also called during the drag so the grid follows.
DataGridColumnPicker.sortStateDataGridSortStateDefault: —The sorted column shows an arrow in the menu, even while hidden.
DataGridColumnPicker.align"start" | "center" | "end"Default: "end"Menu alignment relative to the button. Inside a Toolbar, the button takes the Toolbar size and the subtle appearance.
useDataGridSort / useDataGridSelection / useDataGridColumnVisibility / sortItems / getVisibleColumns / getOrderedColumnshook / hook / hook / (items, columns, sortState) => T[] / (columns, hiddenColumns) => columns / (columns, columnOrder) => columnsDefault: —Logic you can use on its own, for example for server-side sorting, to drive the selection from a toolbar, or to build your own column picker.

Messages

Text built into the component: labels, aria-labels, screen reader announcements.

English by default. Change it with the messages prop of YugenProvider, by the namespace below; messagesVi has the Vietnamese set. See Interface language.

messages.dataGrid

Key · TypeDefault (English)messagesVi
selectRowstring

Tên ô chọn của từng hàng (và tiêu đề cột chọn khi chọn một hàng).

Default (English)"Select row"messagesVi"Chọn hàng"
selectAllstring

Tên ô chọn tất cả ở hàng tiêu đề.

Default (English)"Select all rows"messagesVi"Chọn tất cả hàng"
sortAscendingstring

Chữ ẩn sau tên cột đang sắp xếp.

Default (English)"(ascending)"messagesVi"(tăng dần)"
sortDescendingstringDefault (English)"(descending)"messagesVi"(giảm dần)"
resizeColumnstring

Tên tay nắm đổi độ rộng cột (ghép trước tên cột).

Default (English)"Resize column"messagesVi"Đổi độ rộng cột"
emptyTitlestringDefault (English)"No data"messagesVi"Không có dữ liệu"
emptyDescriptionstringDefault (English)"There are no rows to display."messagesVi"Chưa có hàng nào để hiển thị."
columnsstring

Nhãn nút của DataGridColumnPicker.

Default (English)"Columns"messagesVi"Cột"
showAllColumnsstring

Mục cuối menu của DataGridColumnPicker.

Default (English)"Show all columns"messagesVi"Hiện tất cả cột"
hiddenColumns(count: number) => string

Mô tả nút DataGridColumnPicker khi có cột bị ẩn.

Default (English)(count) => (count === 1 ? "1 column hidden" : `${count} columns hidden`)messagesVi(count) => `${count} cột đang ẩn`