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. This enables grids to work seamlessly in 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';
function MyGrid({ data }) { return ( <DataGrid rows={data} columns={[ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]} responsive={{ breakpoint: 500 }} style={{ height: '400px' }} /> );}<script setup>import '@toolbox-web/grid-vue/features/responsive';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
const data = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' },];</script>
<template> <TbwGrid :rows="data" :responsive="{ breakpoint: 500 }" style="height: 400px"> <TbwGridColumn field="id" header="ID" /> <TbwGridColumn field="name" header="Name" /> <TbwGridColumn field="email" header="Email" /> </TbwGrid></template>// Feature import - enables the [responsive] inputimport { GridResponsiveDirective } from '@toolbox-web/grid-angular/features/responsive';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-my-grid', imports: [Grid, GridResponsiveDirective], template: ` <tbw-grid [rows]="rows" [columns]="columns" [responsive]="{ breakpoint: 500 }" 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' }, ];
columns: ColumnConfig[] = [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ];}Interactive Demo
Section titled “Interactive Demo”<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');let currentBreakpoint = 500;
function rebuild(breakpoint = 500, hideHeader = true, animate = true, hiddenColumns: string[] = [], debounceMs = 100) { currentBreakpoint = breakpoint; grid.gridConfig = { columns, features: { responsive: { breakpoint, hideHeader, animate, hiddenColumns, debounceMs } }, }; grid.rows = sampleData;}
rebuild();
grid.on('responsive-change', ({ isResponsive, width, breakpoint }) => { status.textContent = `Mode: ${isResponsive ? '📱 Card' : '📊 Table'} | Width: ${Math.round(width)}px | Breakpoint: ${breakpoint}px`;});
requestAnimationFrame(() => { const width = grid.clientWidth; status.textContent = `Mode: ${width < currentBreakpoint ? '📱 Card' : '📊 Table'} | Width: ${Math.round(width)}px | Breakpoint: ${currentBreakpoint}px`;});Resize the container to see the grid switch between table and card layouts.
Manual Control
Section titled “Manual Control”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/responsive';
// Sample dataconst 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 },];
// Controls const controls = document.querySelector('[data-controls-id="responsive-manual-control-demo"]'); controls.style.cssText = 'margin-bottom: 12px; display: flex; gap: 8px;';
const tableBtn = document.createElement('button'); tableBtn.textContent = '📊 Table Mode'; tableBtn.style.cssText = 'padding: 8px 16px; cursor: pointer;';
const cardBtn = document.createElement('button'); cardBtn.textContent = '📱 Card Mode'; cardBtn.style.cssText = 'padding: 8px 16px; cursor: pointer;';
controls.appendChild(tableBtn); controls.appendChild(cardBtn);
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns: columns.slice(0, 4), // Fewer columns features: { responsive: { breakpoint: 0 } }, }; grid.rows = sampleData;
tableBtn.addEventListener('click', () => grid.getPluginByName('responsive')?.setResponsive(false)); cardBtn.addEventListener('click', () => grid.getPluginByName('responsive')?.setResponsive(true));Use the plugin API to manually control responsive mode.
Custom Card Renderer
Section titled “Custom Card Renderer”<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;}
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: { breakpoint: 600, cardRenderer: (row: Employee) => { 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; }, }, },};
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' },];
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`;});Use cardRenderer for complete control over card layout - avatars, badges, custom layouts, etc.
Fixed Card Height
Section titled “Fixed Card Height”<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');
interface Employee { id: number; name: string; department: string; salary: number; email: string; startDate: string;}
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 }, ], features: { responsive: { breakpoint: 500, cardRowHeight: 60, cardRenderer: (row: Employee) => { const card = document.createElement('div'); card.className = 'simple-card'; card.innerHTML = ` <div class="initial">${row.name[0]}</div> <div class="details"> <div class="name">${row.name}</div> <div class="dept">${row.department}</div> </div> `; return card; }, }, },};
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' },];Use cardRowHeight for consistent card heights, which helps virtualization performance.
Progressive Degradation
Section titled “Progressive Degradation”<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);Use multiple breakpoints to progressively hide columns before switching to card layout.
Value-Only Columns
Section titled “Value-Only Columns”<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');
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: { breakpoint: 500, hiddenColumns: ['startDate', { field: 'email', showValue: 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' },];Use enhanced hiddenColumns syntax to show values without labels.
Animated Transitions
Section titled “Animated Transitions”<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 }, ], features: { responsive: { breakpoint: 500, animate: true, animationDuration: 300, }, },};
grid.rows = [ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000 }, { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000 }, { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000 }, { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000 },];
grid.on('responsive-change', ({ isResponsive }) => { if (status) status.textContent = isResponsive ? '📱 Card mode - watch the fade-in animation!' : '📊 Table mode';});Smooth animations when transitioning between modes (enabled by default).
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 header row in responsive mode |
debounce-ms | number | Resize debounce delay in ms |
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.
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”Resize the browser window below the breakpoint (600 px) to trigger card mode.
<tbw-grid style="height: 280px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number', align: 'right' }, ], features: { responsive: { breakpoint: 600 }, },};
grid.rows = [ { name: 'Alice Johnson', department: 'Engineering', salary: 95000 }, { name: 'Bob Smith', department: 'Marketing', salary: 75000 }, { name: 'Carol White', department: 'Sales', salary: 68000 }, { name: 'David Brown', department: 'Engineering', salary: 92000 }, { name: 'Eve Davis', department: 'HR', salary: 65000 },];
const log = document.querySelector('#responsive-events-log');const clearBtn = document.querySelector('#clear-responsive-events-log');
function addLog(type: string, detail: string) { if (!log) return; const msg = document.createElement('div'); msg.innerHTML = `<span class="event-type">[${type}]</span> ${detail}`; log.insertBefore(msg, log.firstChild); while (log.children.length > 15) log.lastChild?.remove();}
clearBtn?.addEventListener('click', () => { if (log) log.innerHTML = ''; });
grid.on('responsive-change', (d) => { addLog('responsive-change', `mode: ${d.mode}, width: ${d.width}px`);});responsive-change
Section titled “responsive-change”Emitted when the grid crosses the breakpoint threshold:
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 animations are enabled |
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 Degradation
Section titled “Progressive Degradation”// 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 |
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