Data

Selection

Single or multiple row selection with a checkbox column, keyboard ranges, clipboard copy and CSV export. Selection addresses the rows the user can see, so it indexes the filtered set rather than the raw data.

Basic Usage

Click the checkboxes, Shift-click for a range, or focus a row and press Space. The header checkbox reports all, some or none, so a partial selection reads as a dash rather than a guess.

The whole checkbox cell is the click target, not just the 18 pixels of the checkbox, since this is the one column a user aims at casually. The checkbox keeps the role and the name, so what a screen reader hears is unchanged.

0 selected header: none nothing selected
Email
Status
Hoang Kowalski
hoang.kowalski1@example.com
Design
invited
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
invited
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
active
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
suspended
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
active
$129,812.00
Quyen Tanaka
quyen.tanaka6@example.com
Growth
suspended
$72,011.00
Mai Nguyen
mai.nguyen7@example.com
Support
invited
$83,226.00
Bruno Novak
bruno.novak8@example.com
Support
active
$71,507.00
40 rows
1–8 of 40
<!-- The shorthand: defaults are multiple selection with a checkbox column
     pinned to the left edge -->
<DataGrid data={people} {columns} {getRowId} selection toolbar pageSize={8} />

<script lang="ts">
  import { createDataGrid, selection } from '@sv5ui/datagrid';

  // The same thing, with the feature registered by hand
  const grid = createDataGrid<Person>({
    data: people,
    columns,
    getRowId,
    features: [selection({ mode: 'multiple', checkbox: true })]
  });
</script>

Single Selection

mode: 'single' keeps at most one row, so selecting another releases the first.

Name
Email
Team
Hoang Kowalski
hoang.kowalski1@example.com
Design
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Bruno Dubois
bruno.dubois3@example.com
Design
Farid Haddad
farid.haddad4@example.com
Growth
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Quyen Tanaka
quyen.tanaka6@example.com
Growth
6 rows
// One row at a time. Selecting another replaces the first.
selection({ mode: 'single' })

// No checkbox column: rows are still selectable through the keyboard,
// clicks and the API, but nothing is drawn for it.
selection({ checkbox: false })

Rows That Cannot Be Selected

Suspended members below refuse selection: their checkbox is disabled, select-all skips them, and a Shift range passes over them rather than stopping at them.

Name
Email
Team
Status
Salary
Hoang Kowalski
hoang.kowalski1@example.com
Design
invited
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
invited
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
active
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
suspended
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
active
$129,812.00
Quyen Tanaka
quyen.tanaka6@example.com
Growth
suspended
$72,011.00
Mai Nguyen
mai.nguyen7@example.com
Support
invited
$83,226.00
Bruno Novak
bruno.novak8@example.com
Support
active
$71,507.00
Grace Dubois
grace.dubois9@example.com
Support
active
$94,212.00
Tuan Ivanov
tuan.ivanov10@example.com
Design
active
$105,520.00
10 rows
// Rows that refuse selection are skipped by select-all and by range
// selection, and their checkbox renders disabled.
selection({
  isRowSelectable: (person) => person.status !== 'suspended'
})

Clipboard

Copy writes tab-separated values, which is what a spreadsheet expects from a paste. The synthetic checkbox column is never part of the output.

Tick a row or two, press the button, then paste into the box to see exactly what was written. Ctrl or Cmd + C on a focused cell does the same thing without the button.

nothing copied yet
Name
Email
Team
Status
Hoang Kowalski
hoang.kowalski1@example.com
Design
invited
Bruno Nguyen
bruno.nguyen2@example.com
Growth
invited
Bruno Dubois
bruno.dubois3@example.com
Design
active
Farid Haddad
farid.haddad4@example.com
Growth
suspended
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
active
Quyen Tanaka
quyen.tanaka6@example.com
Growth
suspended
6 rows
// Tab-separated, which is what a spreadsheet expects from a paste
await getSelection(grid)?.copySelection();
await getSelection(grid)?.copySelection({ headers: true });

// The same text without touching the clipboard, for a custom flow
const tsv = getSelection(grid)?.copyText({ headers: true });

// Ctrl+C on a focused grid does the same thing, and emits rowsCopied
grid.events.on('rowsCopied', ({ count }) => toast(`Copied ${count} rows`));

CSV Export

The toolbar export menu offers the selection or every row the filter left. The same thing is available as a call, with more control over the file than a menu can offer.

Each button writes a real file. Narrow the grid with the quick filter first to see what allRows means: it is every row the filter left, not every row in the data.

nothing exported yet
Email
Status
Joined
Hoang Kowalski
hoang.kowalski1@example.com
Design
invited
$127,691.00
Mar 6, 2024
Bruno Nguyen
bruno.nguyen2@example.com
Growth
invited
$105,146.00
Mar 6, 2024
Bruno Dubois
bruno.dubois3@example.com
Design
active
$86,538.00
Feb 5, 2021
Farid Haddad
farid.haddad4@example.com
Growth
suspended
$76,443.00
May 4, 2022
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
active
$129,812.00
Nov 13, 2024
Quyen Tanaka
quyen.tanaka6@example.com
Growth
suspended
$72,011.00
Feb 17, 2019
Mai Nguyen
mai.nguyen7@example.com
Support
invited
$83,226.00
Sep 10, 2022
Bruno Novak
bruno.novak8@example.com
Support
active
$71,507.00
Jan 27, 2022
Grace Dubois
grace.dubois9@example.com
Support
active
$94,212.00
Oct 7, 2024
Tuan Ivanov
tuan.ivanov10@example.com
Design
active
$105,520.00
Jul 11, 2024
Bruno Andersen
bruno.andersen11@example.com
Growth
active
$62,389.00
May 3, 2025
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
suspended
$139,212.00
Aug 23, 2021
12 rows
getSelection(grid)?.exportCsv({
  filename: 'members.csv',
  headers: true,

  // Every filtered row rather than the selection. Also what happens
  // automatically when nothing is selected.
  allRows: false,

  // Excel follows the machine's list separator, so much of Europe needs ';'
  delimiter: ',',

  // Ids in this order. Hidden columns are fair game, because they are
  // resolved against every column rather than the visible ones.
  columns: ['name', 'email', 'team', 'salary', 'country'],

  // Without it a value is written raw, so a spreadsheet keeps its type.
  // With it, the file reads the way the grid does.
  formatValue: ({ value, column }) =>
    column.id === 'salary' ? `$${value}` : String(value ?? '')
});

Every row the filter left is every row the grid is holding, which under rowModel: 'server' is one page. The toolbar says so, naming the item Loaded rows there rather than promising the rest, and onExportAll is how it becomes the whole set again: the server row model page covers it. A selection is unaffected by the row model, since the ids are held across pages.

What a File Holds

Values are written raw by default, because a spreadsheet wants a number it can sum rather than $127,691.00. Raw is not the same as untouched, though: a date has to be readable as a date at the other end.

A date column writes the calendar day the cell drew, and a datetime column the local date and time, rather than the UTC instant toISOString would write, which is the previous day wherever the clock is ahead of Greenwich. A column holding an epoch number comes out as a date rather than as a number. The same goes for a copy to the clipboard.

formatted is the other direction: it writes what the grid is showing, formatting and all, for a file meant to be read rather than summed. formatValue wins over both, being the caller saying something specific.

A feature can stand in front of the value before any of that happens. A cellValue gate is asked for the 'export' and 'clipboard' purposes as well as for the cell, so a column masked on screen leaves masked in the file and on the clipboard rather than only looking masked.

// Raw by default: a spreadsheet gets a number it can sum. This is the
// "Export three columns" button above, and its first line.
selection.exportCsv({
  filename: 'members-short.csv',
  allRows: true,
  columns: ['name', 'salary', 'joinedAt']
});
// Name,Salary,Joined            <- the column headers, not the ids
// Hoang Kowalski,127691,2024-03-06

// A date column writes the day the cell drew, and a datetime column the
// local date and time (2024-03-06T09:30:00). toISOString would write the
// UTC instant, which is the previous day wherever the clock is ahead of
// Greenwich, and an epoch column would leave as a number.

// Formatted: what the grid is showing, for a file meant to be read.
selection.exportCsv({
  filename: 'members-short.csv',
  allRows: true,
  columns: ['name', 'salary', 'joinedAt'],
  formatted: true
});
// Name,Salary,Joined
// Hoang Kowalski,"$127,691.00","Mar 6, 2024"

// The clipboard takes the same option.
selection.copySelection({ headers: true, formatted: true });

// formatValue wins over both, per cell.
selection.exportCsv({
  formatValue: ({ value, column }) =>
    column.id === 'salary' ? `${Number(value) / 1000}k` : String(value ?? '')
});

Export Building Blocks

The pieces the export path is made of are exported too, for a download flow of your own or a format the grid does not produce.

rowsToMatrix, pickColumns, withHeaderRow, toCsv and toTsv. The formula guard is inside toCsv rather than beside it, so a path built from these is as safe as the built-in one.

import { pickColumns, rowsToMatrix, toCsv, toTsv, withHeaderRow } from '@sv5ui/datagrid';

// The export path in pieces, for a download flow of your own or a format
// the grid does not produce.
const matrix = rowsToMatrix(nodes, pickColumns(grid, ['name', 'salary']));
const csv = toCsv(withHeaderRow(matrix, ['Name', 'Salary']), ';');
const tsv = toTsv(matrix);

// A cell starting with = + - or @ is a formula to a spreadsheet, and a
// hostile one is a real attack. toCsv neutralizes them itself, so an export
// path built from these pieces is guarded the same way the built-in one is.

// exportCsv and copySelection take formatted: write what the grid shows
// rather than the value behind it. Off by default, because a spreadsheet
// wants a number it can sum and a date it can sort.
selection.exportCsv({ filename: 'people.csv', formatted: true });
selection.copySelection({ headers: true, formatted: true });

Driving the Selection

getSelection(grid) holds the state; the same methods are merged into grid.api under slightly longer names.

The badges below read count and allState directly, so they update with every click without a subscription of their own, and every button is a call rather than a gesture.

count 0 allState none
Name
Email
Team
Status
Hoang Kowalski
hoang.kowalski1@example.com
Design
invited
Bruno Nguyen
bruno.nguyen2@example.com
Growth
invited
Bruno Dubois
bruno.dubois3@example.com
Design
active
Farid Haddad
farid.haddad4@example.com
Growth
suspended
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
active
Quyen Tanaka
quyen.tanaka6@example.com
Growth
suspended
6 rows
import { getSelection } from '@sv5ui/datagrid';

const selection = getSelection(grid);

selection?.selectedIds;      // ReadonlySet<string>, reactive
selection?.count;            // how many rows are selected
selection?.allState;         // 'none' | 'some' | 'all'
selection?.isSelected('42');
selection?.getSelectedRows(); // TRow[], in pipeline order

selection?.select('42');
selection?.deselect('42');
selection?.toggle('42');
selection?.selectRangeTo('58');   // from the anchor row
selection?.selectAll();
selection?.toggleAll();
selection?.clear();

// The same through grid.api
grid.api.selectRow?.('42');
grid.api.clearSelection?.();
grid.api.getSelectedRows?.();

grid.events.on('selectionChanged', ({ selectedIds }) => console.log(selectedIds));

selection() Options

OptionDefault
mode'multiple'
checkboxtrue
isRowSelectable() => true

Selection State

MemberDescription
selectedIdsThe selected row ids
countHow many rows are selected
allStateWhat the header checkbox shows, including the indeterminate middle
isSelected(id)Membership test
getSelectedRows()The selected rows themselves, in pipeline order
select / deselect / toggleOne row
selectRangeTo(id)Extends the selection from the anchor row, as Shift-click does
selectAll / toggleAll / clearselectAll adds the selectable rows in view, toggleAll takes them back out, clear empties the selection
copySelection(opts)Writes the selection to the clipboard as TSV
copyText(opts)The same text, without touching the clipboard
exportCsv(opts)Builds a CSV and downloads it

ExportCsvOptions

OptionDefault
filename'export.csv'
headerstrue
allRowsfalse
delimiter','
columnsevery visible column
formatValue-
formattedfalse

Keyboard and Mouse

InputAction
SpaceToggles the focused row
Shift + SpaceExtends the selection from the anchor row
Ctrl or Cmd + ASelects every selectable row
Ctrl or Cmd + CCopies the selection as TSV
Space on the header checkboxToggles select-all. The checkbox itself is out of the tab order, so the cell carries it
Shift + ClickSelects the range between the anchor row and this one
Ctrl or Cmd + ClickToggles one row without clearing the rest