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 '@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], 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”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`;});<div class="responsive-instruction" style="margin-bottom: 8px; padding: 0 8px; color: var(--sl-color-gray-3); font-size: 14px;"> ↔ Drag the right edge to resize the container</div><div class="responsive-resize-wrap" style="resize: horizontal; overflow: auto; width: 700px; min-width: 250px; max-width: 100%; border: 2px dashed var(--sl-color-gray-5); padding: 8px;"> <tbw-grid style="height: 350px;"></tbw-grid> <div class="responsive-status" style="margin-top: 8px; font-size: 12px; color: var(--sl-color-gray-3);"></div></div>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”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');if (!grid) throw new Error('Grid not found');
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`;});<div style="margin-bottom: 8px; color: var(--sl-color-gray-3); font-size: 14px;"> ↔ Resize to see custom card layout </div> <div style="resize: horizontal; overflow: auto; width: 400px; min-width: 300px; max-width: 100%; border: 2px dashed var(--sl-color-gray-5); padding: 8px; padding-bottom: 16px;"> <tbw-grid style="height: 400px;"></tbw-grid> <div class="responsive-status" style="margin-top: 8px; font-size: 12px; color: var(--sl-color-gray-3);"></div> </div></div>
<style is:global> #responsive-custom-card-renderer-demo .employee-card { display: flex; align-items: center; gap: 12px; padding: 8px 0; } #responsive-custom-card-renderer-demo .employee-card .avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--tbw-color-accent, #4a90d9); color: white; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: bold; flex-shrink: 0; } #responsive-custom-card-renderer-demo .employee-card .info { flex: 1; min-width: 0; } #responsive-custom-card-renderer-demo .employee-card .name { font-weight: 600; font-size: 16px; color: var(--tbw-color-fg); } #responsive-custom-card-renderer-demo .employee-card .meta { font-size: 13px; color: var(--tbw-color-fg-muted, #666); margin-top: 2px; } #responsive-custom-card-renderer-demo .employee-card .email { font-size: 12px; color: var(--tbw-color-accent, #4a90d9); margin-top: 4px; } #responsive-custom-card-renderer-demo .employee-card .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-left: 8px; } #responsive-custom-card-renderer-demo .employee-card .badge.engineering { background: #e3f2fd; color: #1565c0; } #responsive-custom-card-renderer-demo .employee-card .badge.marketing { background: #f3e5f5; color: #7b1fa2; } #responsive-custom-card-renderer-demo .employee-card .badge.sales { background: #e8f5e9; color: #2e7d32; }</style>Use cardRenderer for complete control over card layout - avatars, badges, custom layouts, etc.
Fixed Card Height
Section titled “Fixed Card Height”import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const grid = queryGrid('tbw-grid');if (!grid) throw new Error('Grid not found');
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' },];<div style="font-size: 12px; color: var(--sl-color-gray-3); margin-bottom: 8px;"> Each card has a fixed 60px height </div> <tbw-grid style="height: 400px;"></tbw-grid></div>
<style is:global> #responsive-fixed-card-height-demo .simple-card { display: flex; align-items: center; gap: 12px; height: 100%; } #responsive-fixed-card-height-demo .simple-card .initial { width: 40px; height: 40px; border-radius: 8px; background: var(--tbw-color-accent, #4a90d9); color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; } #responsive-fixed-card-height-demo .simple-card .details { flex: 1; } #responsive-fixed-card-height-demo .simple-card .name { font-weight: 600; } #responsive-fixed-card-height-demo .simple-card .dept { font-size: 13px; color: var(--tbw-color-fg-muted, #666); }</style>Use cardRowHeight for consistent card heights, which helps virtualization performance.
Progressive Degradation
Section titled “Progressive Degradation”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');if (!grid) throw new Error('Grid not found');
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);<div style="margin-bottom: 8px; color: var(--sl-color-gray-3); font-size: 14px;"> ↔ Resize to see columns hide progressively</div><div style="resize: horizontal; overflow: auto; width: 950px; min-width: 300px; max-width: 100%; border: 2px dashed var(--sl-color-gray-5); padding: 8px; padding-bottom: 16px;"> <tbw-grid style="height: 350px;"></tbw-grid> <div class="responsive-status" style="margin-top: 8px; font-size: 12px; color: var(--sl-color-gray-3);"></div></div>Use multiple breakpoints to progressively hide columns before switching to card layout.
Value-Only Columns
Section titled “Value-Only Columns”import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/responsive';
const grid = queryGrid('tbw-grid');if (!grid) throw new Error('Grid not found');
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' },];<div style="margin-bottom: 8px; color: var(--sl-color-gray-3); font-size: 14px;"> Email shows without label, Start Date is fully hidden</div><div style="width: 400px; border: 2px dashed var(--sl-color-gray-5); padding: 8px;"> <tbw-grid style="height: 400px;"></tbw-grid></div>Use enhanced hiddenColumns syntax to show values without labels.
Animated Transitions
Section titled “Animated Transitions”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');if (!grid) throw new Error('Grid not found');
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';});<div style="margin-bottom: 8px; color: var(--sl-color-gray-3); font-size: 14px;"> ↔ Resize below 500px to see animated transition</div><div style="resize: horizontal; overflow: auto; width: 600px; min-width: 300px; max-width: 100%; border: 2px dashed var(--sl-color-gray-5); padding: 8px; padding-bottom: 16px;"> <tbw-grid style="height: 350px;"></tbw-grid> <div class="responsive-status" style="margin-top: 8px; font-size: 12px; color: var(--sl-color-gray-3);"></div></div>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.
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