Row Selection
Single/multiple selection, checkboxes
Selection lets users pick one or many rows — for bulk actions, exporting just the selected rows, etc. Selection is off by default; you turn it on with the selection group.
What "selection" means here: the grid keeps a private list of which row IDs are currently ticked. It does not change your data — selecting a row just adds its ID to that list and highlights the row. You then read the list (via an event or the API) to do something with those rows: delete them, export them, show a "3 selected" toolbar, and so on.
The three selection modes
The mode option decides how many rows can be ticked at once:
| Mode | Behaviour | Typical use |
|---|---|---|
'none' |
Selection is off (the default). | Read-only tables. |
'single' |
At most one row selected; picking another clears the first — like radio buttons. | "Choose one record to view." |
'multiple' |
Any number of rows; checkboxes and a "select all" header. | Bulk delete/export. |
Turning selection on
const grid = createGrid<User>({
columns,
data,
selection: {
mode: 'multiple', // 'none' | 'single' | 'multiple'
},
})
Setting mode to anything other than 'none' automatically shows a checkbox column.
Selection options
| Property | Type | Default | Meaning |
|---|---|---|---|
mode |
'none' | 'single' | 'multiple' |
'none' |
How many rows can be selected |
selectAllScope |
'page' | 'all' |
'page' |
Does the header checkbox select the page or everything? |
showCheckboxes |
boolean |
true (when mode ≠ none) |
Show the checkbox column |
selectOnRowClick |
boolean |
true |
Clicking anywhere in a row selects it |
checkboxOnly |
boolean |
false |
Only the checkbox selects (clicking the row does nothing) |
Single selection (radio-style)
selection: { mode: 'single' }
Multiple selection with a "select all" that covers everything
The checkbox in the header is "select all". selectAllScope decides what "all" means when pagination or virtualization is in play:
'page'(default) — ticks only the rows currently visible on this page. Safer: the user can clearly see everything they just selected.'all'— ticks every row that passes the current filter, even ones on other pages or scrolled out of view.
selection: {
mode: 'multiple',
selectAllScope: 'all', // header checkbox selects ALL rows, not just this page
}
Checkbox-only selection
Useful when clicking a row should do something else (like open a detail panel) and only the checkbox should toggle selection:
selection: {
mode: 'multiple',
checkboxOnly: true,
selectOnRowClick: false,
}
Reacting to selection changes
onSelectionChange receives the array of selected row objects (not just IDs), so you can act on the data directly:
events: {
onSelectionChange: (selectedRows) => {
console.log(`${selectedRows.length} rows selected`)
console.log('Names:', selectedRows.map((r) => r.name))
},
}
Controlling selection from code (the API)
api.selectRow('42') // select by row ID
api.deselectRow('42')
api.toggleRow('42') // flip its state
api.selectAll()
api.deselectAll()
api.isRowSelected('42') // → boolean
api.getSelectedRows() // → TData[] (the row objects)
api.getSelectedIds() // → Set<string> (just the IDs)
getSelectedRows()vsgetSelectedIds()— which do I want?
- Use
getSelectedRows()when you need the data — names to display, fields to send to a server.- Use
getSelectedIds()when you only need identity — e.g. to pass toapi.deleteRows([...]). It returns aSet, a JavaScript collection of unique values. ASethas no array methods like.map, so spread it into an array first when you need those:[...api.getSelectedIds()].
Remember: these use the row ID, which comes from your
rowIdsetting (default fieldid). See Chapter 01.
Example — a bulk delete button
import { buildGridAPI } from '@elitegrid/vanilla'
const api = buildGridAPI(grid)
const deleteBtn = document.querySelector<HTMLButtonElement>('#delete-selected')!
deleteBtn.addEventListener('click', () => {
const ids = [...api.getSelectedIds()]
api.deleteRows(ids) // bulk delete in one pass
api.deselectAll()
})
See Chapter 12 for buildGridAPI() vs. waiting for onReady.
Selection survives paging and filtering
Selection is tracked by row ID, independently of what's currently on screen — not by "which checkboxes are visible right now." That means:
- Selecting rows on page 1, then moving to page 2, keeps page 1's rows selected.
- Applying a filter that hides a selected row does not deselect it — it's just not visible while the filter is active. Clear the filter and it will still show as checked.
This is almost always what users expect ("I picked these 5 rows, don't lose them because I scrolled"), but it means a selection count in your UI should come from api.getSelectedIds().size (or a live subscription — see below), not from counting checked boxes on the current page.
If you specifically want "select all" to mean only this page rather than every filtered row, that's what selectAllScope: 'page' (the default) already does — see above.
Reading selection reactively. Rather than manually tracking a count via
onSelectionChange, subscribe directly to theselectionnamespace ongrid.kernel.store— it calls your function again every time the selection changes, and hands back anunsubscribefunction:const badge = document.querySelector<HTMLSpanElement>('#selection-count')! function render(state: { count: number; selectedIds: Set<string>; isAllSelected: boolean }) { badge.textContent = `${state.count} selected` } render(grid.kernel.store.read('selection')) const unsubscribe = grid.kernel.store.subscribe('selection', render)
Pre-selecting rows on load
To open the grid with certain rows already selected (e.g. restoring a saved selection, or highlighting rows passed in from another page), select them in onReady:
const grid = createGrid<User>({
columns,
data,
selection: { mode: 'multiple' },
events: {
onReady: (api) => {
const preselectedIds = ['12', '47', '88']
preselectedIds.forEach((id) => api.selectRow(id))
},
},
})
Live example
Checkbox multi-select with a live "N selected" readout, using the grid.kernel.store.subscribe('selection', …) pattern introduced above.
import { createGrid, mount, type GridAPI } from '@elitegrid/vanilla'
import '@elitegrid/vanilla/styles.css'
interface Employee {
id: number
name: string
department: string
role: string
}
const employees: Employee[] = [
{ id: 1, name: 'Ada Lovelace', department: 'Engineering', role: 'Staff Engineer' },
{ id: 2, name: 'Alan Turing', department: 'Research', role: 'Principal Scientist' },
{ id: 3, name: 'Grace Hopper', department: 'Engineering', role: 'Eng Manager' },
{ id: 4, name: 'Margaret Hamilton', department: 'Engineering', role: 'Tech Lead' },
{ id: 5, name: 'Katherine Johnson', department: 'Research', role: 'Senior Analyst' },
{ id: 6, name: 'Linus Torvalds', department: 'Engineering', role: 'Staff Engineer' },
{ id: 7, name: 'Tim Berners-Lee', department: 'Research', role: 'Principal Scientist' },
{ id: 8, name: 'Barbara Liskov', department: 'Engineering', role: 'Eng Manager' },
{ id: 9, name: 'Dennis Ritchie', department: 'Engineering', role: 'Staff Engineer' },
{ id: 10, name: 'Radia Perlman', department: 'Research', role: 'Senior Analyst' },
]
let api: GridAPI<Employee> | null = null
const grid = createGrid<Employee>({
columns: [
{ field: 'name', header: 'Name', size: { flex: 2 } },
{ field: 'department', header: 'Department', size: { flex: 1.5 } },
{ field: 'role', header: 'Role', size: { flex: 1.5 } },
],
data: employees,
selection: { mode: 'multiple' },
events: { onReady: (a) => { api = a } },
})
const container = document.getElementById('grid-container')!
container.style.cssText = 'height:440px;display:flex;flex-direction:column;gap:8px;padding:8px;box-sizing:border-box'
const toolbar = document.createElement('div')
toolbar.style.cssText = 'display:flex;align-items:center;gap:12px;flex-shrink:0'
const clearBtn = document.createElement('button')
clearBtn.textContent = 'Clear selection'
clearBtn.style.cssText = 'padding:7px 16px;border-radius:7px;border:none;cursor:pointer;' +
'font-size:0.8rem;font-weight:600;font-family:system-ui;background:#7c3aed;color:#ffffff'
clearBtn.onclick = () => api?.deselectAll()
toolbar.appendChild(clearBtn)
const badge = document.createElement('span')
badge.style.cssText = 'font-size:0.8rem;color:#a1a1aa;font-family:system-ui'
toolbar.appendChild(badge)
container.appendChild(toolbar)
function render(state: { count: number }) {
badge.textContent = `${state.count} selected`
}
render(grid.kernel.store.read('selection'))
grid.kernel.store.subscribe('selection', render)
const gridEl = document.createElement('div')
gridEl.style.cssText = 'flex:1;min-height:0'
container.appendChild(gridEl)
mount(grid, gridEl)
Common selection mistakes
| Symptom | Cause | Fix |
|---|---|---|
| "3 selected" badge shows the wrong count after filtering | Counting checked DOM checkboxes instead of reading the API/store | Use api.getSelectedIds().size or subscribe to grid.kernel.store on the 'selection' namespace as the source of truth |
selectRow('42') does nothing |
The ID passed doesn't match any row's rowId |
Double-check your rowId setting (see Chapter 01) — a mismatched type (42 vs '42') also silently fails to match |
| Clicking a row toggles selection when it shouldn't | selectOnRowClick defaults to true |
Set selectOnRowClick: false (optionally with checkboxOnly: true) if rows should only select via their checkbox |
| "Select all" selected far more rows than expected | selectAllScope: 'all' selects every filtered row, not just the visible page |
Use the default 'page' scope, or make the scope explicit in your UI copy ("Select all 240 results") so it isn't a surprise |
Next: 07 · Inline Editing