Skip to content

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.

import '@toolbox-web/grid/features/responsive';
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;

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.

ModeAuto follows the breakpoint; table/card force it via setResponsive()
Breakpoint (px)Card layout applies below this width
Debounce (ms)Minimum interval between layout switches
Hide field labelsDrop the per-card "Name:" prefix
Hidden columnsColumns to drop in card mode
Hidden column styleRemove the cell entirely, or keep the value without its label
AnimationCross-fade the grid, or travel each row — or each cell — into place
Duration (ms)
↔ Drag the right edge to resize the container
responsive-change log

A few things worth trying:

  • Modetable and card bypass the breakpoint by calling setResponsive(). The next resize across the breakpoint takes over again.
  • Hidden column stylehide removes the cell; value only keeps the value and drops its label (the object form of hiddenColumns).
  • Motionanimate off switches instantly. morph rows animates each row from its table position into its card position instead of cross-fading the grid as a whole; see Animated transitions.

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.

Card heightA fixed height lets virtualization skip measuring every card
↔ Resize to see custom card layout

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.

↔ Resize to see columns hide progressively

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:

ValueBehaviour
falseNo 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.

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.

AttributeTypeDescription
breakpointnumberWidth threshold in pixels for responsive mode
card-row-heightnumber | 'auto'Card height in pixels or auto
hidden-columnsstringComma-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-msnumberMinimum interval in ms between layout switches

See ResponsivePluginConfig for the full list of options and defaults.

For progressive degradation, use the breakpoints array instead of a single breakpoint. See BreakpointConfig for the full interface.

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
]

The breakpoint should be based on your grid’s column count and content:

Grid SizeSuggested Breakpoint
3-5 columns400-500px
6-10 columns600-800px
10+ columns900-1200px

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`);
});

Control responsive mode programmatically via the plugin instance:

// Get the plugin instance from the grid
const plugin = grid.getPluginByName('responsive');
// Check current mode
const isCardMode = plugin.isResponsive();
// Force responsive mode (regardless of width)
plugin.setResponsive(true);
plugin.setResponsive(false);
// Update breakpoint dynamically
plugin.setBreakpoint(600);
// Get current grid width
const width = plugin.getWidth();
// Get active breakpoint (multi-breakpoint mode)
const activeBreakpoint = plugin.getActiveBreakpoint();
// Returns: { maxWidth: 600, hiddenColumns: [...], cardLayout: false } or null
  1. ResizeObserver monitors the grid element’s width
  2. When width < breakpoint, the plugin adds data-responsive attribute to the grid
  3. CSS transforms cells from horizontal to vertical layout
  4. Each cell displays “Header: Value” using the ::before pseudo-element with data-header attribute

This CSS-only approach means:

  • No DOM replacement or re-rendering needed
  • Smooth transitions between modes
  • Works with all other plugins (selection, editing, etc.)

The responsive plugin uses the grid’s existing CSS custom properties for theming.

The responsive plugin uses the grid’s built-in CSS variables:

PropertyDescription
--tbw-cell-paddingPadding inside card rows
--tbw-color-borderCard separator color
--tbw-color-bgCard background color
--tbw-color-row-altAlternating card background
--tbw-color-row-hoverCard hover background
--tbw-color-selectionSelected card background
--tbw-color-header-fgLabel text color
--tbw-color-accentSelection indicator color
/* 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;
}
AttributeElementDescription
data-responsivetbw-gridPresent when in responsive (card) mode
data-responsive-animatetbw-gridPresent when the CSS keyframe fade is in use (no view transition support)
data-responsive-transitiontbw-gridPresent while a view transition is driving the layout switch
data-header.cellColumn header text for CSS ::before
data-responsive-hidden.cellMarks cells hidden via hiddenColumns
data-responsive-value-only.cellMarks cells showing value only (no label)
PropertyDefaultDescription
--tbw-responsive-duration200msAnimation duration for mode transitions

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:

KeyTable ModeResponsive Mode
Previous rowPrevious field (within card), wraps to previous card
Next rowNext field (within card), wraps to next card
Previous columnPrevious card (same field)
Next columnNext card (same field)
TabNext cell, wraps to next rowSame behavior
EnterStart editingSame behavior
EscapeCancel editingSame behavior

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.

// Grid in a resizable panel
features: {
responsive: {
breakpoint: 400,
hiddenColumns: ['createdAt', 'updatedAt'], // Hide dates in card mode
},
},
// Responsive grid for mobile/tablet
features: {
responsive: {
breakpoint: 768,
hideHeader: true,
},
},
// Small widget in dashboard
features: {
responsive: {
breakpoint: 300,
hiddenColumns: ['email', 'phone', 'address'],
},
},
// Gracefully degrade as container shrinks
features: {
responsive: {
breakpoints: [
{ maxWidth: 900, hiddenColumns: ['startDate'] },
{ maxWidth: 700, hiddenColumns: ['startDate', 'email'] },
{ maxWidth: 500, cardLayout: true },
],
},
},

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: (row: T, rowIndex: number) => HTMLElement
ParameterTypeDescription
rowTThe row data object
rowIndexnumberIndex of the row in the data array
ReturnsHTMLElementThe 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.

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) => { /* ... */ },
},
},

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-activate event
  • Escape - Standard escape handling
  • Selection — Row and cell selection
  • Theming — CSS custom properties for styling