Data Grid
A spreadsheet-grade grid for working with tabular data: inline cell editing, multi-column sorting, pinned columns, row selection, and virtualized rendering. Columns are defined as a JavaScript array, and the grid implements the full WAI-ARIA grid keyboard pattern with a single tab stop.
<arc-data-grid> Overview
DataGrid is the whole tabular family in one component. Columns are configured through a columns array property (not child elements), where each entry can opt into sorting, inline editing, alignment, a fixed width, and left-edge pinning. Nothing is on by default that a read-only table would not want, so a plain display grid is this component with the interactive props left alone.
It absorbed arc-table and arc-data-table in v4, which is why that is true. Three props came across and they are the ones a read-mostly table cares about. density="compact" tightens cell padding, from Table. striped draws alternating row backgrounds — it defaults on, because this grid has always striped unconditionally and a merge is not the place to restyle the survivor, so no-striped is the migration path for a plain Table. overscan sets how many rows render above and below the visible window when virtual is on; it was public on arc-virtual-list and a hard-coded 5 here, a divergence that survived precisely because the two copies of the arithmetic never met. Raising it trades DOM nodes for fewer blank rows on a fling.
Migrating: Table's positional columns: ["A"] / rows: [["1"]] become named keys — [{ key, label }] and one object per row — so a column can move without every row moving with it. Data Table's slotted <arc-column> children become one columns entry each with the same field names, and its sort-column/sort-direction pair becomes a single entry in the multi-sort sort array, where one entry behaves exactly as the pair did. Selection, virtual scrolling and overscan are unchanged. See the tombstones for Table, Data Table and Column.
Sorting is multi-column: clicking a sortable header cycles ascending → descending → off, and Shift+clicking appends the column as a secondary sort. When more than one sort is active, each sorted header shows its direction arrow plus a priority number. The grid sorts a copy of your data internally and also emits arc-sort with the full sort array — set manual-sort to skip internal sorting and drive it from a server instead.
Editable columns turn cells into inline editors: press Enter or double-click a cell to open a token-styled input, Enter or blur commits (emitting arc-cell-change), Escape cancels. The grid mutates only its own display copy — your source array stays untouched, so you remain the owner of the data. Pinned columns stay stuck to the left edge during horizontal scroll with an elevation shadow, and the virtual mode renders only visible rows for large datasets.
Keyboard support follows the WAI-ARIA grid pattern: one tab stop for the whole grid, arrow keys move a roving cell focus, Home/End jump to row ends, Ctrl+Home/End jump to the grid corners, Enter activates (sorts a header, edits a cell), and Space toggles row selection.
Guidelines
When to use
- Use DataGrid for read-mostly display too — leave `selectable` off and no column `editable`, and it is a table
- Reach for `density="compact"` and `no-striped` when the grid is displaying rather than editing; that combination is what a plain `arc-table` looked like before it merged in here
- Listen to arc-cell-change and write edits back to your own data store — the grid only updates its display copy
- Set manual-sort and handle arc-sort yourself when the dataset is paginated or sorted server-side
- Pin only one or two key identifier columns (IDs, names) so unpinned data stays readable
- Give pinned columns an explicit width — pinned offsets are computed from column widths
- Enable `virtual` with an accurate `row-height` for datasets beyond a few hundred rows
- Raise `overscan` if fast scrolling shows blank rows, and lower it if the row count in the DOM is the problem
When not to use
- Do not mark every column editable — restrict editing to fields users genuinely need to change inline
- Do not rely on the grid to persist edits; it never mutates the rows array you passed in
- Do not pin so many columns that unpinned content has no room on narrow screens
- Do not mix virtual mode with rows of varying heights — virtualization assumes a fixed `row-height`
- Do not set `overscan` high to paper over a wrong `row-height`; the buffer hides the symptom and every scroll position stays slightly wrong
- Do not nest complex interactive components (modals, drawers) inside grid cells
- Do not reach for DataGrid to render a list of one thing per row — that is `arc-list`, and a grid is the wrong semantics for it
Features
- Column configuration via a plain JavaScript array — width, alignment, sortable, editable, pinned per column
- Multi-column sorting: click cycles asc/desc/none, Shift+click adds secondary sorts with priority indicators
- manual-sort mode for server-side sorting driven by the `arc-sort` event
- Inline cell editing with Enter/double-click to open, Enter/blur to commit, Escape to cancel
- Grid mutates only its internal display copy — consumer data stays the source of truth
- Pinned columns stick to the left edge with an elevation shadow while scrolling horizontally
- Row selection with select-all checkbox including indeterminate state
- Virtualized rendering for large datasets via the `virtual` and `row-height` props, with `overscan` controlling the buffer
- `density="compact"` reduces cell padding for dense data displays (absorbed from `arc-table`)
- `striped` alternating row backgrounds, on by default — `no-striped` for a plain table
- Full WAI-ARIA grid keyboard pattern: roving cell focus, one tab stop, arrow/Home/End/Ctrl navigation
- `arc-sort`, `arc-cell-change`, and `arc-select` custom events
- Sticky header row that stays visible during vertical scroll
- CSS custom property theming via ARC design tokens, plus ::part hooks for table, header, row, cell, and editor
Preview
| No data available |
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<arc-data-grid id="positions-grid" selectable></arc-data-grid>
<!-- Read-mostly, the way a plain arc-table looked: no selection, no
editable columns, tighter rows, no stripes. -->
<arc-data-grid id="positions-readonly" density="compact" no-striped></arc-data-grid>
<script type="module">
import '@arclux/arc-ui/data-grid';
const grid = document.querySelector('#positions-grid');
grid.columns = [
{ key: 'ticker', label: 'Ticker', width: '90px', pinned: true, sortable: true },
{ key: 'name', label: 'Company', width: '180px', sortable: true },
{ key: 'sector', label: 'Sector', sortable: true },
{ key: 'price', label: 'Price', align: 'right', sortable: true, editable: true },
{ key: 'change', label: 'Change %', align: 'right', sortable: true, editable: true }
];
grid.rows = [
{ ticker: 'ARC', name: 'Arclight Systems', sector: 'Technology', price: 142.5, change: 2.4 },
{ ticker: 'LUM', name: 'Lumen Dynamics', sector: 'Energy', price: 87.2, change: -1.1 },
{ ticker: 'NVA', name: 'Nova Materials', sector: 'Industrials', price: 56.8, change: 0.7 }
];
grid.addEventListener('arc-cell-change', (e) => {
const { rowIndex, key, value } = e.detail;
// write the edit back to your own data store
console.log('edited', rowIndex, key, value);
});
grid.addEventListener('arc-sort', (e) => console.log('sort', e.detail.sort));
grid.addEventListener('arc-select', (e) => console.log('selected', e.detail.selectedIndices));
</script> import { DataGrid } from '@arclux/arc-ui-react';
const columns = [
{ key: 'ticker', label: 'Ticker', width: '90px', pinned: true, sortable: true },
{ key: 'name', label: 'Company', width: '180px', sortable: true },
{ key: 'sector', label: 'Sector', sortable: true },
{ key: 'price', label: 'Price', align: 'right', sortable: true, editable: true },
{ key: 'change', label: 'Change %', align: 'right', sortable: true, editable: true }
];
const positions = [
{ ticker: 'ARC', name: 'Arclight Systems', sector: 'Technology', price: 142.5, change: 2.4 },
{ ticker: 'LUM', name: 'Lumen Dynamics', sector: 'Energy', price: 87.2, change: -1.1 },
{ ticker: 'NVA', name: 'Nova Materials', sector: 'Industrials', price: 56.8, change: 0.7 }
];
export function PositionsGrid() {
return (
<DataGrid
columns={columns}
rows={positions}
selectable
onArcCellChange={(e) => console.log('edited', e.detail)}
onArcSort={(e) => console.log('sort', e.detail.sort)}
onArcSelectionChange={(e) => console.log('selected', e.detail.selectedIndices)}
/>
);
} <script setup>
import { DataGrid } from '@arclux/arc-ui-vue';
const columns = [
{ key: 'ticker', label: 'Ticker', width: '90px', pinned: true, sortable: true },
{ key: 'name', label: 'Company', width: '180px', sortable: true },
{ key: 'sector', label: 'Sector', sortable: true },
{ key: 'price', label: 'Price', align: 'right', sortable: true, editable: true },
{ key: 'change', label: 'Change %', align: 'right', sortable: true, editable: true }
];
const positions = [
{ ticker: 'ARC', name: 'Arclight Systems', sector: 'Technology', price: 142.5, change: 2.4 },
{ ticker: 'LUM', name: 'Lumen Dynamics', sector: 'Energy', price: 87.2, change: -1.1 },
{ ticker: 'NVA', name: 'Nova Materials', sector: 'Industrials', price: 56.8, change: 0.7 }
];
</script>
<template>
<DataGrid
:columns="columns"
:rows="positions"
selectable
@arc-cell-change="(e) => console.log('edited', e.detail)"
@arc-sort="(e) => console.log('sort', e.detail.sort)"
/>
</template> <script>
import { DataGrid } from '@arclux/arc-ui-svelte';
const columns = [
{ key: 'ticker', label: 'Ticker', width: '90px', pinned: true, sortable: true },
{ key: 'name', label: 'Company', width: '180px', sortable: true },
{ key: 'sector', label: 'Sector', sortable: true },
{ key: 'price', label: 'Price', align: 'right', sortable: true, editable: true },
{ key: 'change', label: 'Change %', align: 'right', sortable: true, editable: true }
];
const positions = [
{ ticker: 'ARC', name: 'Arclight Systems', sector: 'Technology', price: 142.5, change: 2.4 },
{ ticker: 'LUM', name: 'Lumen Dynamics', sector: 'Energy', price: 87.2, change: -1.1 },
{ ticker: 'NVA', name: 'Nova Materials', sector: 'Industrials', price: 56.8, change: 0.7 }
];
</script>
<DataGrid
{columns}
rows={positions}
selectable
on:arc-cell-change={(e) => console.log('edited', e.detail)}
on:arc-sort={(e) => console.log('sort', e.detail.sort)}
/> import { Component } from '@angular/core';
import { DataGrid } from '@arclux/arc-ui-angular';
@Component({
imports: [DataGrid],
template: `
<arc-data-grid
[columns]="columns"
[rows]="positions"
selectable
(arcCellChange)="onCellChange($event)"
(arcSort)="onSort($event)"
></arc-data-grid>
`,
})
export class PositionsGridComponent {
columns = [
{ key: 'ticker', label: 'Ticker', width: '90px', pinned: true, sortable: true },
{ key: 'name', label: 'Company', width: '180px', sortable: true },
{ key: 'sector', label: 'Sector', sortable: true },
{ key: 'price', label: 'Price', align: 'right', sortable: true, editable: true },
{ key: 'change', label: 'Change %', align: 'right', sortable: true, editable: true }
];
positions = [
{ ticker: 'ARC', name: 'Arclight Systems', sector: 'Technology', price: 142.5, change: 2.4 },
{ ticker: 'LUM', name: 'Lumen Dynamics', sector: 'Energy', price: 87.2, change: -1.1 },
{ ticker: 'NVA', name: 'Nova Materials', sector: 'Industrials', price: 56.8, change: 0.7 }
];
onCellChange(e: CustomEvent) {
console.log('edited', e.detail);
}
onSort(e: CustomEvent) {
console.log('sort', e.detail.sort);
}
} API
-
columnsArray<{key:string,label:string,sortable?:boolean,editable?:boolean,pinned?:boolean,width?:string,align?:string}>[] - Column definitions. Each entry maps a
keyin your row objects to a rendered column with alabelheader. Optional flags enable sorting, inline editing, and inline-start-edge pinning per column;widthsets a fixed CSS width (required for accurate pinned offsets) andaligncontrols text alignment. Pinned columns are always displayed first. Set via JavaScript property. -
rowsArray<Record<string, any>>[] - The data array. Each object becomes a row keyed by column
key. The grid works on an internal shallow copy — sorting and inline edits never mutate the array you pass in. Set via JavaScript property; reassigning it resets selection and any open editor. -
sortArray<{key:string,direction:'asc'|'desc'}>[] - Multi-sort state in priority order. Clicking a sortable header cycles it asc → desc → none; Shift+click appends it as a secondary sort. When more than one sort is active, headers show a direction arrow plus priority number. Set this property to pre-sort the grid.
-
manualSortbooleanfalse - Skips internal sorting. Rows render in the order given, while headers still cycle the
sortstate and emitarc-sort— use this to implement server-side sorting. -
selectablebooleanfalse - Adds a checkbox column with a select-all header checkbox (indeterminate when partially selected). Space toggles selection from the keyboard. Emits
arc-selectwith the selected row indices. -
virtualbooleanfalse - Enables virtual scrolling for large datasets. Only visible rows plus an overscan buffer are rendered, keeping performance constant regardless of row count.
-
rowHeightnumber40 - Height in pixels of each row when virtual scrolling is enabled. Must match the actual rendered row height for correct scroll calculations.
-
overscannumber5 - Rows rendered above and below the visible window when
virtualis on, to cover fast scrolling. Raising it trades DOM nodes for fewer blank rows on a fling; lowering it does the reverse. Never negative. -
density'default' | 'compact''default' - Row density.
compactreduces cell padding for dense data displays. Absorbed fromarc-table. -
stripedbooleantrue - Alternating row backgrounds. On by default, which is what this grid has always drawn;
no-stripedturns them off, which is what an unstripedarc-tablelooked like.
Events
-
arc-sort - Fired when the user changes sorting. detail: { sort } with the full multi-sort array in priority order
-
arc-select - Fired when row selection changes. detail: { value, selectedIndices } — sorted indices into the original rows array
-
arc-cell-change - Fired when an inline cell edit is committed. detail: { rowIndex, key, value, row } — rowIndex refers to the original rows array