Data Grid
Work with large datasets using filters, selection, column controls, editing, and server-managed state.
Overview
Use Data Grid when the dataset is a main part of the application. It handles advanced filters, page-scoped selection, resizable and reorderable columns, pinned columns, editable cells, server-managed state, and virtualized rows.
Data Grid does not try to copy a spreadsheet. It keeps the native table structure and HonestUI's quiet row styling, then adds controls when someone asks for them. The default view is still a toolbar, column names, rows, and a footer.
Use Table for static rows and columns. Use Data Table for search, simple filters, sorting, selection, and pagination on a smaller record set. Choose Data Grid when people need to customize the columns or work through thousands of records.
Anatomy
- ToolbarSearch, active filters, column controls, and application actions
- FrameOne boundary for rows and footer controls
- ViewportHorizontal and vertical scrolling with an optional sticky header
- TableColumn headers, optional inline filters, data rows, and states
- FooterSelection actions, page range, and pagination
- OverlaysFilter builder, column menu, and row action menus
The default DataGrid composes those parts from feature props. It also exports each part and attaches them to DataGrid, so DataGrid.Toolbar, DataGrid.Viewport, and DataGrid.Pagination can be rearranged without replacing the state model.
Columns use TanStack Table definitions plus Data Grid options such as filter, type, align, editable, and hideBelow. Cell renderers can use any React content. Avatar, Badge, Menu, and Progress remain application choices rather than grid-specific APIs.
Behavior
Sorting cycles through ascending, descending, and unsorted. Hold Shift while choosing another header to build a multi-column sort. The small number beside a sorted header reports its priority.
The filter builder chooses its input from the column's filter type. Applied filters stay visible in the toolbar and return pagination to the first page. Inline header filters are optional because they add weight to every column.
The header checkbox selects eligible rows on the current page. It never claims to select records that the browser has not loaded. Pass a row predicate to selection when only some rows can be selected.
Column headers support pointer reordering. The same menu includes Move left and Move right, so dragging is not the only path. Resize handles accept a pointer, double-click to reset, and Left or Right Arrow in 8px steps. Pinned columns stay inside the scrolling frame.
Accessibility
Pass caption to name the table. Data Grid renders a native <table> with column headers and body rows. Sort direction uses aria-sort. Selection checkboxes expose checked and indeterminate state through HonestUI Checkbox.
When virtualization is enabled, the table exposes the complete row count and each rendered row's position with aria-rowcount and aria-rowindex. These values include header rows. Pass rowCount when the server knows about rows that are not loaded in the browser.
Use getRowLabel to name each row checkbox with the record itself, such as "Select Sarah Chen." The built-in fallback uses a row number, which is less useful when rows move after sorting.
When a row cannot be selected, return a short explanation from getRowSelectionDisabledReason. The grid adds it to the disabled checkbox name, so the reason is available without relying on a tooltip.
Set keyboardNavigation when people need cell movement. Arrow keys move between cells. Home and End move across a row. Ctrl+Home and Ctrl+End move to the first or last grid cell. Enter starts editing or moves into an interactive control. Tab saves an edit and moves to the next cell. Shift+Tab saves and moves to the previous cell. Space toggles row selection. Inputs, selects, menus, and other controls keep their own keyboard behavior.
Wide grids scroll inside DataGridViewport. This is a valid two-dimensional data region, but the toolbar and surrounding page must still reflow at zoom. Test the finished grid with keyboard input, 200% and 400% zoom, a supported screen reader, forced colors, and long translated values.
Installation
Usage
Start with columns and rows, then enable the controls the task needs.
import {
DataGrid,
type DataGridColumn,
} from "@/components/ui/data-grid/data-grid"
const columns: DataGridColumn<User>[] = [
{
accessorKey: "name",
header: "Name",
size: 220,
minSize: 160,
filter: { type: "text" },
},
{
accessorKey: "status",
header: "Status",
filter: {
type: "enum",
options: ["Active", "Pending", "Inactive"],
},
},
{
accessorKey: "revenue",
header: "Revenue",
type: "currency",
align: "right",
},
]
<DataGrid
data={users}
columns={columns}
caption="Users"
search={{ placeholder: "Search users..." }}
filters
sorting
selection
columnVisibility
pagination
/>Compose the shell
Pass children when the default order does not fit the page. The same table instance is available to every part through context.
<DataGrid data={users} columns={columns} selection pagination>
<DataGrid.Toolbar>
<DataGrid.Search placeholder="Search users..." />
<DataGrid.ToolbarSpacer />
<DataGrid.ColumnVisibility />
</DataGrid.Toolbar>
<DataGrid.Frame>
<DataGrid.Viewport>
<DataGrid.Table />
</DataGrid.Viewport>
<DataGrid.Footer>
<DataGrid.SelectionStatus>{bulkActions}</DataGrid.SelectionStatus>
<DataGrid.Pagination />
</DataGrid.Footer>
</DataGrid.Frame>
</DataGrid>Control server state
For server data, pass the rows from the current response, the total row count, controlled state, and matching callbacks. Set each manual* prop for work the server owns.
<DataGrid
data={query.data.rows}
columns={columns}
rowCount={query.data.total}
pageCount={query.data.pageCount}
search={{ placeholder: "Search orders...", debounceMs: 250 }}
globalSearch={globalSearch}
onGlobalSearchChange={setGlobalSearch}
sorting={sorting}
onSortingChange={setSorting}
pagination={pagination}
onPaginationChange={setPagination}
manualSorting
manualFiltering
manualPagination
/>The grid reports intent. Your application owns the request, cancellation, stale response handling, and error message. Data Grid does not require a fetching library.
Do not do this
Do not sort only the loaded server page. Ten rows may appear sorted while the other 9,990 records stay in the wrong order.
Do not place a visible control in the toolbar unless it works. Pass toolbar.refresh only when there is a refresh callback. The built-in CSV action exports the filtered rows available to the table and treats formula-prefixed string values as text. For a complete server export, pass toolbar.export.onExport, let the server build the file, and apply equivalent spreadsheet-injection protection there.
Do not replace wide rows with cards on a narrow screen. Keep the column relationships and contain horizontal scrolling inside the viewport. Hide a lower-priority column with hideBelow only when the application can remove that information safely.
Examples
Filters
The filter builder and inline header filters use text, enum, number, currency, date, and boolean column types. Active filters remain above the rows.
Selection and bulk actions
Select eligible rows on the current page. The footer keeps the count and actions in a stable position.
Column controls
Resize a column from its right edge. Drag a header to reorder it, use the menu as a keyboard alternative, or pin a column while the viewport scrolls.
Cell editing
Double-click an editable cell or focus it and press Enter. The application receives the row, column, previous value, and new value. A custom editor can use Select or another HonestUI control.
Loading, empty, and error states
Switch between the states to inspect the stable header, row skeletons, empty message, and retry action.
Externally controlled data
This example keeps search, sorting, and pagination outside the grid. It applies those values to a local dataset so the state contract is visible without pretending to make a network request.
Virtualized rows
The log grid contains 10,000 rows. TanStack Virtual renders the visible range and a small overscan buffer inside a fixed-height viewport.
Density
Compact uses 44px rows. Default uses 56px. Comfortable uses 64px for cells with more content.
API reference
DataGrid
Controlled state pairs use one convention. Pass globalSearch with onGlobalSearchChange, sorting with onSortingChange, filters with onFiltersChange, pagination with onPaginationChange, and selection with onSelectionChange. Column state uses columnVisibility, columnOrder, columnSizing, and columnPinning with their matching callbacks. Every state also has a default* prop for uncontrolled setup.
Column options
Exported parts
DataGridRoot, DataGridToolbar, DataGridToolbarSpacer, DataGridSearch, DataGridActiveFilters, DataGridFilterTrigger, DataGridColumnVisibility, DataGridExport, DataGridRefresh, DataGridFrame, DataGridViewport, DataGridTable, DataGridFooter, DataGridSelectionStatus, DataGridPagination, and DataGridRowActions are available as named exports. The same parts are attached to DataGrid for the compound form.