Responsive Plugin
The Responsive plugin transforms the grid from a tabular layout to a card/list layout when the grid width falls below a configurable breakpoint. Use it for narrow containers like split-pane UIs, mobile viewports, and dashboard widgets.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/responsive';Basic Usage
Section titled “Basic Usage”import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, },};grid.rows = data;import '@toolbox-web/grid-react/features/responsive';import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, },};
function MyGrid({ data }) { return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />;}<script setup>import '@toolbox-web/grid-vue/features/responsive';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
const data = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' },];
const gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, },};</script>
<template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /></template>Define everything inside gridConfig.features, exactly like the other frameworks. The side-effect import registers the feature so the adapter can bridge it.
import '@toolbox-web/grid-angular/features/responsive';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { rows = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ];
gridConfig: GridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { responsive: { breakpoint: 500 }, }, };}Interactive Playground
Section titled “Interactive Playground”Drag the container’s right edge to cross the breakpoint. Every single-breakpoint option is wired to a
control, and each responsive-change event is logged underneath.
<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const sampleData = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },];const columns = [ { field: 'id', header: 'ID', type: 'number', width: 60 }, { field: 'name', header: 'Name', width: 150 }, { field: 'department', header: 'Department', width: 120 }, { field: 'salary', header: 'Salary', type: 'number', width: 100 }, { field: 'email', header: 'Email', width: 200 }, { field: 'startDate', header: 'Start Date', width: 120 },];
const grid = queryGrid('tbw-grid');const status = document.querySelector('.responsive-status');const log = document.querySelector('#responsive-default-log');document.querySelector('#responsive-default-clear').addEventListener('click', () => (log.innerHTML = ''));
interface Values { mode: string; breakpoint: number; debounceMs: number; hideHeader: boolean; hiddenColumns: string[]; hiddenStyle: string; animation: string; animationDuration: number;}
let current: Values = { mode: 'auto', breakpoint: 500, debounceMs: 100, hideHeader: false, hiddenColumns: [], hiddenStyle: 'hide', animation: 'fade', animationDuration: 200,};
function apply(v: Values) { current = v; grid.gridConfig = { columns, features: { responsive: { breakpoint: v.breakpoint, debounceMs: v.debounceMs, hideHeader: v.hideHeader, hiddenColumns: v.hiddenStyle === 'value only' ? v.hiddenColumns.map((field) => ({ field, showValue: true })) : v.hiddenColumns, animation: v.animation === 'off' ? false : v.animation, animationDuration: v.animationDuration, }, }, }; grid.rows = sampleData;
requestAnimationFrame(() => { forceMode(); render(); });}
/** * Forcing a mode is a plugin-API call, not config — and a `gridConfig` rebuild * detaches and re-attaches the plugin, whose ResizeObserver then re-evaluates * the breakpoint and undoes the override. So re-assert it from the * `responsive-change` handler too; the loop settles after one correction. */function forceMode() { if (current.mode === 'auto') return; const want = current.mode === 'card'; const plugin = grid.getPluginByName('responsive'); if (plugin && plugin.isResponsive() !== want) plugin.setResponsive(want);}
function render() { const plugin = grid.getPluginByName('responsive'); const isCard = plugin?.isResponsive() ?? false; const width = Math.round(plugin?.getWidth() ?? grid.clientWidth); const forced = current.mode === 'auto' ? '' : ` | forced via setResponsive(${current.mode === 'card'})`; status.textContent = `Mode: ${isCard ? '📱 Card' : '📊 Table'} | Width: ${width}px | Breakpoint: ${current.breakpoint}px${forced}`;}
apply(current);
grid.on('responsive-change', ({ isResponsive, width, breakpoint }) => { const entry = document.createElement('div'); entry.innerHTML = `<span class="event-type">[responsive-change]</span> isResponsive: ${isResponsive}, ` + `width: ${Math.round(width)}px, breakpoint: ${breakpoint}px`; log.insertBefore(entry, log.firstChild); while (log.children.length > 15) log.lastChild?.remove(); forceMode(); render();});A few things worth trying:
- Mode —
tableandcardbypass the breakpoint by callingsetResponsive(). The next resize across the breakpoint takes over again. - Hidden column style —
hideremoves the cell;value onlykeeps the value and drops its label (the object form ofhiddenColumns). - Motion —
animateoff switches instantly.morph rowsanimates each row from its table position into its card position instead of cross-fading the grid as a whole; see Animated transitions.
Custom Card Renderer
Section titled “Custom Card Renderer”cardRenderer takes over card rendering completely — avatars, badges, whatever layout you need. Pair
it with a fixed cardRowHeight so virtualization can skip measuring every card.
The renderer owns the card content, but not the row element: the grid still applies your
rowClass on top, so a row highlight looks the same in both layouts.
<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const grid = queryGrid('tbw-grid');const status = document.querySelector('.responsive-status');
interface Employee { id: number; name: string; department: string; salary: number; email: string; startDate: string;}
const cardRenderer = (value: unknown) => { const row = value; const card = document.createElement('div'); card.className = 'employee-card'; const deptClass = row.department.toLowerCase().replace(/\s+/g, '-'); card.innerHTML = ` <div class="avatar">${row.name[0]}</div> <div class="info"> <div class="name"> ${row.name} <span class="badge ${deptClass}">${row.department}</span> </div> <div class="meta">$${row.salary.toLocaleString()} · Started ${row.startDate}</div> <div class="email">${row.email}</div> </div> `; return card;};
const rows = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },];
function apply(cardRowHeight: number | 'auto') { grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', width: 60 }, { field: 'name', header: 'Name', width: 150 }, { field: 'department', header: 'Department', width: 120 }, { field: 'salary', header: 'Salary', type: 'number', width: 100 }, { field: 'email', header: 'Email', width: 200 }, { field: 'startDate', header: 'Start Date', width: 120 }, ], rowClass: (value: unknown) => ((value).salary >= 95000 ? 'top-earner' : ''), features: { responsive: { breakpoint: 600, cardRowHeight, cardRenderer } }, }; grid.rows = rows;}
apply('auto');
grid.on('responsive-change', ({ isResponsive, width }) => { if (!status) return; status.textContent = `Mode: ${isResponsive ? '📱 Custom Cards' : '📊 Table'} | Width: ${Math.round(width)}px`;});
requestAnimationFrame(() => { if (!status) return; const width = grid.clientWidth; status.textContent = `Mode: ${width < 600 ? '📱 Custom Cards' : '📊 Table'} | Width: ${Math.round(width)}px`;});Progressive Degradation
Section titled “Progressive Degradation”The breakpoints array replaces the single breakpoint, hiding columns in stages before falling all
the way back to card layout.
Hiding or restoring a column is not a layout switch, so it does not go through the view transition. Only the column that changed fades, and it keeps its width until the fade finishes — every other column stays exactly where it is.
<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const grid = queryGrid('tbw-grid');const status = document.querySelector('.responsive-status');
grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', width: 60 }, { field: 'name', header: 'Name', width: 150 }, { field: 'department', header: 'Department', width: 120 }, { field: 'salary', header: 'Salary', type: 'number', width: 100 }, { field: 'email', header: 'Email', width: 200 }, { field: 'startDate', header: 'Start Date', width: 120 }, ], features: { responsive: { breakpoints: [ { maxWidth: 900, hiddenColumns: ['startDate'] }, { maxWidth: 700, hiddenColumns: ['startDate', 'email'] }, { maxWidth: 500, hiddenColumns: ['startDate', 'email', 'salary'] }, { maxWidth: 400, cardLayout: true }, ], }, },};
grid.rows = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },];
const updateStatus = () => { if (!status) return; const plugin = grid.getPluginByName('responsive'); const bp = plugin?.getActiveBreakpoint(); const isCard = plugin?.isResponsive(); if (!bp) { status.textContent = '📊 Full Table (no columns hidden)'; } else if (isCard) { status.textContent = `📱 Card Layout (≤${bp.maxWidth}px)`; } else { const hidden = bp.hiddenColumns?.length ?? 0; status.textContent = `📊 Table - ${hidden} column(s) hidden (≤${bp.maxWidth}px)`; }};
grid.on('responsive-change', updateStatus);requestAnimationFrame(updateStatus);Animated Transitions
Section titled “Animated Transitions”The animation option picks how the switch between table and card layout is animated, and takes the
same shape as the expand/collapse animation on the tree, grouping and master-detail plugins — a
single union with false as the off value:
| Value | Behaviour |
|---|---|
false | No animation; the layout swaps instantly |
'fade' | Cross-fade the grid as a whole (default) |
'morph-rows' | Travel each row from its table position to its card position |
'morph-cells' | Travel each cell from its column to its stacked position in the card |
Transitions respect animation.mode and prefers-reduced-motion.
Where the browser supports element-scoped view transitions
(Element.startViewTransition(),
Chrome/Edge 147+), the switch is handed to the compositor. The transition is scoped to the grid, so
the rest of your page stays interactive and keeps painting. Browsers without support fall back to a
CSS keyframe fade — the two never run together.
The morph styles give every named element its own compositor layer, so they cost more than the
default. 'morph-cells' falls back to 'morph-rows' above 150 rendered cells; reach for it on
compact grids where the column-to-label movement is the point, and stay on 'morph-rows' for dense
ones.
The budget counts rendered cells, not rows in your data — rows are virtualized, so a 100-row grid names only the cells in the visible window. Each named cell costs three animations (group, old, new) and two snapshot textures, so what governs the cost is viewport height × column count.
Column visibility changes are animated separately, per column — see Progressive Degradation.
Light DOM Configuration
Section titled “Light DOM Configuration”The ResponsivePlugin supports declarative configuration via the <tbw-grid-responsive-card> element. Framework adapters provide wrapper components with idiomatic rendering patterns.
<tbw-grid> <tbw-grid-responsive-card breakpoint="500" card-row-height="80" hidden-columns="createdAt, updatedAt" hide-header="true"> <div class="custom-card"> <strong>{{ row.name }}</strong> <span>{{ row.email }}</span> </div> </tbw-grid-responsive-card></tbw-grid>In vanilla JS, the innerHTML of the element is used as a template. Use {{ row.fieldName }} syntax for value interpolation.
import { DataGrid, GridResponsiveCard } from '@toolbox-web/grid-react';
<DataGrid rows={data} columns={columns} responsive={{ breakpoint: 500 }}> <GridResponsiveCard<Employee> cardRowHeight={80}> {({ row, index }) => ( <div className="custom-card"> <strong>{row.name}</strong> <span>{row.email}</span> </div> )} </GridResponsiveCard></DataGrid>React uses a render function as children, receiving { row, index }.
<script setup>import { TbwGrid, TbwGridResponsiveCard } from '@toolbox-web/grid-vue';</script>
<template> <TbwGrid :rows="data" :columns="columns" :responsive="{ breakpoint: 500 }"> <TbwGridResponsiveCard> <template #default="{ row, rowIndex }"> <div class="custom-card"> <strong>{{ row.name }}</strong> <span>{{ row.email }}</span> </div> </template> </TbwGridResponsiveCard> </TbwGrid></template>Vue uses a scoped slot, receiving { row, rowIndex }.
<tbw-grid [rows]="data" [columns]="columns" [responsive]="{ breakpoint: 500 }"> <tbw-grid-responsive-card> <ng-template let-row let-index="index"> <div class="custom-card"> <strong>{{ row.name }}</strong> <span>{{ row.email }}</span> </div> </ng-template> </tbw-grid-responsive-card></tbw-grid>Angular uses <ng-template> with implicit context binding (let-row, let-index="index"). Import GridResponsiveCard from @toolbox-web/grid-angular.
Vanilla Attributes
Section titled “Vanilla Attributes”| Attribute | Type | Description |
|---|---|---|
breakpoint | number | Width threshold in pixels for responsive mode |
card-row-height | number | 'auto' | Card height in pixels or auto |
hidden-columns | string | Comma-separated field names to hide |
hide-header | 'true' | 'false' | Hide the per-card field label (e.g. Name:). Default 'false'. Does not affect the column header row, which is always hidden in card mode. |
debounce-ms | number | Minimum interval in ms between layout switches |
Configuration Options
Section titled “Configuration Options”See ResponsivePluginConfig for the full list of options and defaults.
BreakpointConfig
Section titled “BreakpointConfig”For progressive degradation, use the breakpoints array instead of a single breakpoint. See BreakpointConfig for the full interface.
HiddenColumnConfig
Section titled “HiddenColumnConfig”The enhanced hiddenColumns syntax supports both simple strings and objects. See HiddenColumnConfig for the full type definition.
Top-level hiddenColumns applies to the card layout only — the table keeps every column. To drop columns from the table as it narrows, put hiddenColumns on a breakpoint instead; those tracks collapse so the grid width follows the visible columns.
Example:
hiddenColumns: [ 'startDate', // Fully hidden { field: 'email', showValue: true }, // Value shown without label]Choosing a Breakpoint
Section titled “Choosing a Breakpoint”The breakpoint should be based on your grid’s column count and content:
| Grid Size | Suggested Breakpoint |
|---|---|
| 3-5 columns | 400-500px |
| 6-10 columns | 600-800px |
| 10+ columns | 900-1200px |
Events
Section titled “Events”responsive-change
Section titled “responsive-change”Emitted when the grid crosses the breakpoint threshold. The interactive playground logs every one of these live.
grid.on('responsive-change', ({ isResponsive, width, breakpoint }) => { console.log(isResponsive ? 'Card mode' : 'Table mode'); console.log(`Width: ${width}px, Breakpoint: ${breakpoint}px`);});Programmatic API
Section titled “Programmatic API”Control responsive mode programmatically via the plugin instance:
// Get the plugin instance from the gridconst plugin = grid.getPluginByName('responsive');
// Check current modeconst isCardMode = plugin.isResponsive();
// Force responsive mode (regardless of width)plugin.setResponsive(true);plugin.setResponsive(false);
// Update breakpoint dynamicallyplugin.setBreakpoint(600);
// Get current grid widthconst width = plugin.getWidth();
// Get active breakpoint (multi-breakpoint mode)const activeBreakpoint = plugin.getActiveBreakpoint();// Returns: { maxWidth: 600, hiddenColumns: [...], cardLayout: false } or nullHow It Works
Section titled “How It Works”- ResizeObserver monitors the grid element’s width
- When
width < breakpoint, the plugin addsdata-responsiveattribute to the grid - CSS transforms cells from horizontal to vertical layout
- Each cell displays “Header: Value” using the
::beforepseudo-element withdata-headerattribute
This CSS-only approach means:
- No DOM replacement or re-rendering needed
- Smooth transitions between modes
- Works with all other plugins (selection, editing, etc.)
Styling
Section titled “Styling”The responsive plugin uses the grid’s existing CSS custom properties for theming.
CSS Custom Properties
Section titled “CSS Custom Properties”The responsive plugin uses the grid’s built-in CSS variables:
| Property | Description |
|---|---|
--tbw-cell-padding | Padding inside card rows |
--tbw-color-border | Card separator color |
--tbw-color-bg | Card background color |
--tbw-color-row-alt | Alternating card background |
--tbw-color-row-hover | Card hover background |
--tbw-color-selection | Selected card background |
--tbw-color-header-fg | Label text color |
--tbw-color-accent | Selection indicator color |
Custom Card Styling
Section titled “Custom Card Styling”/* Custom card styling */tbw-grid[data-responsive] .data-grid-row { border-radius: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); margin-bottom: 8px;}
/* Hide specific columns in card mode */tbw-grid[data-responsive] .cell[data-field="createdAt"],tbw-grid[data-responsive] .cell[data-field="updatedAt"] { display: none;}Data Attributes
Section titled “Data Attributes”| Attribute | Element | Description |
|---|---|---|
data-responsive | tbw-grid | Present when in responsive (card) mode |
data-responsive-animate | tbw-grid | Present when the CSS keyframe fade is in use (no view transition support) |
data-responsive-transition | tbw-grid | Present while a view transition is driving the layout switch |
data-header | .cell | Column header text for CSS ::before |
data-responsive-hidden | .cell | Marks cells hidden via hiddenColumns |
data-responsive-value-only | .cell | Marks cells showing value only (no label) |
CSS Custom Properties for Animation
Section titled “CSS Custom Properties for Animation”| Property | Default | Description |
|---|---|---|
--tbw-responsive-duration | 200ms | Animation duration for mode transitions |
Keyboard Shortcuts
Section titled “Keyboard Shortcuts”In responsive mode, the visual layout is inverted - cells are stacked vertically within each card. The plugin automatically adjusts keyboard navigation to match this layout:
| Key | Table Mode | Responsive Mode |
|---|---|---|
| ↑ | Previous row | Previous field (within card), wraps to previous card |
| ↓ | Next row | Next field (within card), wraps to next card |
| ← | Previous column | Previous card (same field) |
| → | Next column | Next card (same field) |
| Tab | Next cell, wraps to next row | Same behavior |
| Enter | Start editing | Same behavior |
| Escape | Cancel editing | Same behavior |
Custom Card Renderers
Section titled “Custom Card Renderers”When using cardRenderer (Phase 2), the grid’s built-in keyboard navigation is disabled for arrow keys. Implementors should handle navigation within their custom card content via their own event handlers.
Use Cases
Section titled “Use Cases”Split-Pane UI
Section titled “Split-Pane UI”// Grid in a resizable panelfeatures: { responsive: { breakpoint: 400, hiddenColumns: ['createdAt', 'updatedAt'], // Hide dates in card mode },},Mobile-First Design
Section titled “Mobile-First Design”// Responsive grid for mobile/tabletfeatures: { responsive: { breakpoint: 768, hideHeader: true, },},Dashboard Widget
Section titled “Dashboard Widget”// Small widget in dashboardfeatures: { responsive: { breakpoint: 300, hiddenColumns: ['email', 'phone', 'address'], },},Progressive column hiding
Section titled “Progressive column hiding”// Gracefully degrade as container shrinksfeatures: { responsive: { breakpoints: [ { maxWidth: 900, hiddenColumns: ['startDate'] }, { maxWidth: 700, hiddenColumns: ['startDate', 'email'] }, { maxWidth: 500, cardLayout: true }, ], },},Custom Card Renderer
Section titled “Custom Card Renderer”For advanced card layouts with avatars, badges, or custom grouped fields, use the cardRenderer option:
features: { responsive: { breakpoint: 600, cardRenderer: (row, rowIndex) => { const card = document.createElement('div'); card.className = 'employee-card'; card.innerHTML = ` <div class="avatar">${row.name[0]}</div> <div class="info"> <div class="name">${row.name}</div> <div class="meta">${row.department} · $${row.salary.toLocaleString()}</div> <div class="email">${row.email}</div> </div> `; return card; }, },},cardRenderer Signature
Section titled “cardRenderer Signature”cardRenderer: (row: T, rowIndex: number) => HTMLElement| Parameter | Type | Description |
|---|---|---|
row | T | The row data object |
rowIndex | number | Index of the row in the data array |
| Returns | HTMLElement | The element to render as the card content |
The returned element becomes the card’s content. The surrounding row element is still the grid’s,
so gridConfig.rowClass classes are applied to it after the renderer runs — do not try to set them
from inside the card.
cardRowHeight
Section titled “cardRowHeight”When using cardRenderer, you can control card height:
// Fixed height (better for virtualization with large datasets)features: { responsive: { breakpoint: 600, cardRowHeight: 80, // 80px per card cardRenderer: (row) => { /* ... */ }, },},
// Auto height (default - cards size to content)features: { responsive: { breakpoint: 600, cardRowHeight: 'auto', cardRenderer: (row) => { /* ... */ }, },},Keyboard Navigation with cardRenderer
Section titled “Keyboard Navigation with cardRenderer”When using a custom cardRenderer, the grid’s built-in arrow key navigation is disabled. This allows you to implement your own navigation within the card content.
Standard keyboard shortcuts still work:
- Tab - Move between cards
- Enter - Triggers
cell-activateevent - Escape - Standard escape handling