Skip to content

Selection Plugin

The Selection plugin adds cell, row, and range selection capabilities to the grid with full keyboard support. Whether you need simple cell highlighting or complex multi-range selections, this plugin has you covered.

The Selection plugin exists to maintain a selection state — a set of cells, rows, or ranges — that other plugins can act on. It is the prerequisite for things like:

  • ClipboardPlugin — copy/paste the current selection
  • ExportPlugin — when onlySelected: true, export only the rows the user has selected (without it, all rows are exported)
  • ContextMenuPlugin — when present, the context menu will operate on the multi-row selection instead of just the right-clicked row (the menu itself works fine without it)
  • Any custom logic that needs to know “which rows/cells did the user pick?”

If all you want is to visually highlight the row the user has navigated to (the focused row), you do not need this plugin. The grid core already tracks keyboard focus on the active cell via the .cell-focus class — you can style the surrounding row with a small CSS snippet:

/* Highlight the row containing the focused cell, no plugin required */
tbw-grid .data-grid-row:has(.cell-focus) {
background-color: var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%));
}
/* Optional: paint the focus tint over sticky/pinned cells too,
which otherwise have an opaque background to mask scrolling content */
tbw-grid .data-grid-row:has(.cell-focus) > .cell.sticky-left,
tbw-grid .data-grid-row:has(.cell-focus) > .cell.sticky-right {
background:
linear-gradient(var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),
var(--tbw-color-panel-bg);
}
/* Optional: suppress the per-cell focus outline if you want a row-only
indicator. The grid core paints `outline: var(--tbw-focus-outline)` on
`.cell-focus` while the grid has focus; leaving it ON makes it easier
for keyboard users to see which cell they're on inside the highlighted
row. Only add this rule if you specifically want the row to be the sole
focus indicator (mimicking the Selection plugin's row mode).
The grid core sets the `data-has-focus` attribute on the host element
whenever focus is inside the grid (managed by the focus controller),
and removes it on blur — so this rule only suppresses the cell outline
while the grid is actively focused. */
tbw-grid[data-has-focus] .cell-focus {
outline: none;
}

This mirrors what the Selection plugin does for row mode focus — without shipping the plugin’s selection-state machinery, keyboard range extension, or click handlers. Reach for the plugin only when something downstream needs to read or react to a selection.

import '@toolbox-web/grid/features/selection';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/selection';
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Carol', email: 'carol@example.com' },
];
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' },
{ field: 'email', header: 'Email' },
],
features: {
selection: 'row',
},
};
grid.rows = data;

Switch between selection modes to see how each one behaves. The state panel below the grid shows the current selection in real time.

  • Cell mode: Click cells to select them individually
  • Row mode: Click anywhere in a row to select the entire row. Ctrl+Click to toggle, Shift+Click for range
  • Range mode: Click and drag to select rectangular ranges. Ctrl+drag for multiple ranges
  • Column mode: Ctrl/⌘+Click on a header (or Ctrl/⌘+Space on a focused cell) selects that column. Ctrl+Shift+Click extends from the column anchor.
ModeSelection mode. "row+column" enables both axes (mutually exclusive at runtime). Use Ctrl/⌘+Click on a header (or Ctrl/⌘+Space) to select a column.
Interact with the grid to see state updates...

Add checkbox: true to show a selection checkbox column with a “select all” header. Works exclusively in row mode.

features: { selection: { mode: 'row', checkbox: true } }
Since 2.8.0

Set mode: 'column' to enable column-axis selection. Selected columns are tracked by field name (not visible-index), so the selection survives column pinning, reordering, virtualization recycling, and visibility changes.

features: { selection: { mode: 'column' } }

You can combine column selection with one in-row axis ('cell', 'row', or 'range') by passing an array. The two axes are mutually exclusive at any given moment — selecting on one clears the other and announces the axis change to assistive technology:

features: { selection: { mode: ['row', 'column'] } }

Invalid combinations (['cell', 'row'], ['cell', 'range'], ['row', 'range']) throw at attach time — only 'column' + X array shapes are accepted.

Activation paths

InputAction
Ctrl/⌘ + Click on column headerToggle column
Ctrl/⌘ + Shift + Click on column headerExtend from anchor
Ctrl/⌘ + SpaceToggle column at focused cell
Ctrl/⌘ + Shift + ←/→Extend column selection
Plain header clickReserved for sort (no selection change)

Utility columns (checkbox, expander, etc.) are never selectable. With multiSelect: false, only one column can be selected at a time and selectAllColumns() is a no-op.

You can disable selection grid-wide using gridConfig.selectable:

grid.gridConfig = {
selectable: false, // Disables ALL selection
features: { selection: 'range' },
};

See SelectionConfig for the full list of options and defaults.

In data-entry grids, you may want single-click to only focus the row/cell for keyboard navigation, while double-click changes the selection state.

features: {
selection: {
mode: 'row',
triggerOn: 'dblclick', // Single-click focuses, double-click selects
},
}

Use the isSelectable callback to prevent selection of specific rows or cells:

features: {
selection: {
mode: 'row',
isSelectable: (row) => row.status !== 'locked',
},
}

Behavior of non-selectable rows/cells:

AspectBehavior
ClickIgnored (no selection change)
KeyboardSkipped with Shift+Arrow
Select AllExcluded
VisualMuted via [data-selectable="false"] attribute
FocusStill navigable

Rows with status "locked" cannot be selected. Try clicking or using Shift+Click — locked rows are skipped.

ShortcutAction
Arrow KeysMove focus
Shift + ArrowExtend selection (row and range modes)
Shift + Page Up/DownExtend selection by page (row and range modes)
Shift + Ctrl/⌘ + Home/EndExtend selection to first/last row (row and range modes)
Ctrl/⌘ + ClickToggle row/cell (multi-select)
Shift + ClickExtend selection from anchor
Ctrl/⌘ + ASelect all (row and range modes)
Ctrl/⌘ + Click (header)Toggle column selection (column mode)
Ctrl/⌘ + Shift + Click (header)Extend column selection from anchor
Ctrl/⌘ + SpaceToggle column at the focused column (column mode)
Ctrl/⌘ + Shift + ←/→Extend column selection (column mode)
EscapeClear current selection (whichever axis is active)

In mode: 'range', right-clicking inside an existing range keeps the current range selection. This lets context-menu actions target the full selected range without requiring Ctrl/⌘.

Right-clicking outside the current range follows normal behavior and moves selection to the clicked cell.

const plugin = grid.getPluginByName('selection');
// Query
const selection = plugin.getSelection();
plugin.isCellSelected(2, 1);
// Row mode
plugin.selectRows([0, 2, 4]);
const rows = plugin.getSelectedRows<Employee>();
// Range mode
plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: 5, col: 3 } }]);
// Column mode (since 2.8.0)
plugin.selectColumn('email');
plugin.selectColumn('name', { toggle: true });
plugin.selectColumn('lastLogin', { range: true }); // From column anchor
plugin.deselectColumn('email');
plugin.selectAllColumns();
plugin.clearColumnSelection();
const cols = plugin.getSelectedColumns(); // string[]
// Actions
plugin.selectAll();
plugin.clearSelection();
EventDescription
selection-changeFired when the selection is modified
Event Log

See SelectionChangeDetail and CellRange for the full event payload types.

PropertyDefaultDescription
--tbw-focus-backgroundrgba(accent, 12%)Focused row background
--tbw-range-selection-bgrgba(accent, 12%)Range selection fill
--tbw-range-border-colorvar(--tbw-color-accent)Range selection border
--tbw-color-accent#3b82f6Primary accent color
tbw-grid {
--tbw-range-selection-bg: rgba(76, 175, 80, 0.15);
--tbw-range-border-color: #4caf50;
--tbw-focus-background: rgba(76, 175, 80, 0.1);
}
ClassElement
.selectingGrid during range drag
.row-focusFocused row (row mode)
.cell-focusFocused cell (cell mode)
.selectedSelected cell in range
.selected.top / .bottom / .first / .lastRange boundary edges
PluginIntegration
EditingPluginClick-to-select + double-click-to-edit. In mode: 'row' the row entering edit is auto-added to the selection so getSelectedRows() always reflects the row the user is visibly editing. With multiSelect: false the selection is replaced; otherwise the edited row is added to the existing set. Selection is also automatically cleared when the host replaces the rows array with a different number of source rows, preventing stale indices from pointing at the wrong rows.
ClipboardPluginCopy/paste selected cells (requires SelectionPlugin)
FilteringPluginFilter data, then select from results
ContextMenuPluginRight-click selected rows for actions. In range mode, right-click inside the existing range preserves that range so actions apply to the full selection.

Range selection is built on a drag: press one cell, sweep to the opposite corner, release. WCAG 2.2 SC 2.5.7 Dragging Movements requires a single-pointer alternative for every such gesture, so mode: 'range' ships two.

  • Extend selection to here — click the first cell as usual, then open the context menu on the opposite corner and pick Extend selection to here. The context menu opens on right-click, long-press, or Shift+F10, so it is reachable with one pointer and no keyboard. Right-clicking outside the current range still re-targets the menu at the clicked cell; the extension anchor is the last cell you selected with a primary click, so the action always extends from where you started.
  • Tap a corner handle — the two dots on an active range are buttons as well as drag targets. Tap one and it arms (aria-pressed="true"); the next cell you tap becomes that corner. Tap the handle again, or press Escape, to cancel. Handles only appear for a range that was started with a finger or stylus — on a mouse they would sit inside other ranges and block drag-select.
  • Merges with the Context Menu plugin — when ContextMenuPlugin is installed, the action is contributed to the normal menu (its own order band, below the column-move group). Without it, the plugin hosts a minimal role="group" menu of plain buttons instead, so the alternative exists either way. The two never stack.
  • No reserved chrome — nothing extra is drawn in the grid body. The alternative reuses affordances that already exist: the cell you would have dragged from and the handle you would have dragged.
  • KeyboardShift+Arrow, Shift+Page Up/Down and Shift+Ctrl/⌘+Home/End also extend a range. Note that keyboard equivalence alone does not satisfy SC 2.5.7: the success criterion is about single-pointer operation, which is why the pointer paths above exist. Shift+Click needs a modifier key and does not count either.

Selection also announces the armed corner through the grid’s live region, and marks selected cells with aria-selected so assistive technology reports the current range.