Accessibility (A11y)
ARIA attributes, keyboard navigation, focus management, and screen reader support.
ZenGrid provides comprehensive accessibility support including ARIA attributes, keyboard navigation, focus management, and screen reader announcements.
Enable Accessibility
Enable accessibility features via the enableA11y option:
import { createGrid } from '@zengrid/core';
const grid = createGrid(container, { columns, rowCount: 100, enableA11y: true, // Enable accessibility features data: (rowIndex, colId) => `Cell ${rowIndex}-${colId}`,});Accessibility is enabled by default. Disable it only if you have a specific reason and provide alternative accessibility support.
FocusManager
The FocusManager handles keyboard focus and navigation within the grid.
getActiveCell()
Get the currently focused cell:
const activeCell = grid.focusManager.getActiveCell();
if (activeCell) { console.log('Focused cell:', { rowIndex: activeCell.rowIndex, colId: activeCell.colId, });} else { console.log('No cell has focus');}setActiveCell()
Programmatically set focus to a cell:
// Set active cell without DOM focusgrid.focusManager.setActiveCell({ rowIndex: 5, colId: 'name' });
// Set active cell with DOM focusgrid.focusManager.setActiveCell( { rowIndex: 5, colId: 'name' }, true // focus = true);Use setActiveCell() with focus: true to programmatically focus a cell and scroll it into view.
moveFocus()
Move focus in a direction:
// Move focus down one cellgrid.focusManager.moveFocus('down', rowCount, colCount);
// Move focus up one cellgrid.focusManager.moveFocus('up', rowCount, colCount);
// Move focus right one cellgrid.focusManager.moveFocus('right', rowCount, colCount);
// Move focus left one cellgrid.focusManager.moveFocus('left', rowCount, colCount);movePageFocus()
Move focus by a page (viewport height):
const pageSize = 20; // Number of rows per page
// Move focus down one pagegrid.focusManager.movePageFocus('down', pageSize, rowCount);
// Move focus up one pagegrid.focusManager.movePageFocus('up', pageSize, rowCount);moveToRowStart()
Move focus to the first cell in the current row:
grid.focusManager.moveToRowStart();moveToRowEnd()
Move focus to the last cell in the current row:
grid.focusManager.moveToRowEnd(colCount);moveToGridStart()
Move focus to the first cell in the grid (row 0, first column):
grid.focusManager.moveToGridStart();moveToGridEnd()
Move focus to the last cell in the grid (last row, last column):
grid.focusManager.moveToGridEnd(rowCount, colCount);focusGrid()
Focus the grid container:
// Focus the grid (not a specific cell)grid.focusManager.focusGrid();hasFocus()
Check if the grid has focus:
const focused = grid.focusManager.hasFocus();
if (focused) { console.log('Grid has focus');} else { console.log('Grid does not have focus');}clearFocus()
Clear focus from all cells:
grid.focusManager.clearFocus();The FocusManager automatically handles scrolling to keep the focused cell visible when moving focus.
Keyboard Navigation
ZenGrid supports comprehensive keyboard navigation following WAI-ARIA grid patterns.
Arrow Keys
Navigate between cells:
// Arrow keys move focus between cells// β Up Arrow: Move focus up one cell// β Down Arrow: Move focus down one cell// β Left Arrow: Move focus left one cell// β Right Arrow: Move focus right one cellTab Key
Move between cells:
// Tab: Move focus to next cell (right, then down)// Shift+Tab: Move focus to previous cell (left, then up)Tab behavior can be customized via the tabNavigationMode option: 'cell' (default) navigates between cells, 'row' navigates between rows, and 'default' uses browser default tab behavior.
Home and End Keys
Navigate to row/grid boundaries:
// Home: Move focus to first cell in current row// End: Move focus to last cell in current row// Ctrl+Home: Move focus to first cell in grid// Ctrl+End: Move focus to last cell in gridPage Up and Page Down
Navigate by pages:
// Page Down: Move focus down one page// Page Up: Move focus up one pageEnter and Escape
Editing interaction:
// Enter: Start editing the focused cell// Escape: Cancel editing and return focus to cellComplete Keyboard Navigation Example
import { createGrid } from '@zengrid/core';
const grid = createGrid(container, { columns, rowCount: 1000, enableA11y: true,
// Configure tab navigation tabNavigationMode: 'cell', // 'cell' | 'row' | 'default'
data: (rowIndex, colId) => `Cell ${rowIndex}-${colId}`,});
// Custom keyboard shortcutscontainer.addEventListener('keydown', (e) => { // Ctrl+A: Select all if (e.ctrlKey && e.key === 'a') { e.preventDefault(); grid.api.exec('selection:selectAll'); }
// Ctrl+C: Copy if (e.ctrlKey && e.key === 'c') { e.preventDefault(); grid.api.exec('clipboard:copy'); }
// Ctrl+V: Paste if (e.ctrlKey && e.key === 'v') { e.preventDefault(); grid.api.exec('clipboard:paste'); }});All keyboard navigation automatically scrolls the focused cell into view. You donβt need to manually handle scrolling.
ARIA Attributes
ZenGrid applies ARIA attributes automatically when accessibility is enabled.
Grid Role
The grid container has role="grid":
<div role="grid" aria-rowcount="1000" aria-colcount="5"> <!-- Grid content --></div>Row Role
Each row has role="row":
<div role="row" aria-rowindex="1"> <!-- Row cells --></div>Cell Role
Each cell has role="gridcell":
<div role="gridcell" aria-colindex="1" tabindex="0"> Cell content</div>Selection State
Selected cells have aria-selected="true":
<div role="gridcell" aria-selected="true" tabindex="0"> Selected cell</div>Sort State
Sortable column headers have aria-sort:
<!-- Not sorted --><div role="columnheader" aria-sort="none"> Name</div>
<!-- Sorted ascending --><div role="columnheader" aria-sort="ascending"> Age β</div>
<!-- Sorted descending --><div role="columnheader" aria-sort="descending"> Age β</div>Labels
Columns can have aria-label for better screen reader support:
const columns: Column[] = [ { id: 'actions', header: '', // No visible text width: 100, ariaLabel: 'Actions', // Screen reader label }, { id: 'status', header: 'β', // Icon width: 60, ariaLabel: 'Status indicator', },];ARIA attributes are applied automatically based on grid state. You donβt need to manually set them in most cases.
Screen Reader Support
ZenGrid provides screen reader announcements for state changes.
Sort Announcements
// When sort state changes, screen readers announce:// "Column Name sorted ascending"// "Column Age sorted descending"// "Sort cleared"Filter Announcements
// When filters change, screen readers announce:// "Filters applied, showing 42 of 100 rows"// "Quick filter applied, showing 15 of 100 rows"// "Filters cleared, showing all 100 rows"Selection Announcements
// When selection changes, screen readers announce:// "Row 5 selected"// "Cell Name at row 5 selected"// "3 rows selected"// "Selection cleared"Edit Announcements
// When editing starts/ends, screen readers announce:// "Editing cell Name at row 5"// "Edit saved"// "Edit cancelled"Custom Announcements
Add custom screen reader announcements:
import { announceToScreenReader } from '@zengrid/core';
// Announce custom messageannounceToScreenReader('Data loaded successfully');
// Announce with priorityannounceToScreenReader('Error: Failed to save', 'assertive');
// Default is 'polite'announceToScreenReader('5 rows updated');Use 'polite' for informational messages and 'assertive' for urgent messages like errors.
Focus Events
Listen to focus events for custom behavior:
// Focus changedgrid.on('focus:change', (event) => { console.log('Focus changed to:', event.cell); // { rowIndex: 5, colId: 'name' }});
// Focus entered gridgrid.on('focus:in', () => { console.log('Grid received focus');});
// Focus left gridgrid.on('focus:out', () => { console.log('Grid lost focus');});
// Cell focusedgrid.on('cell:focus', (event) => { console.log('Cell focused:', event.rowIndex, event.colId);});
// Cell blurredgrid.on('cell:blur', (event) => { console.log('Cell blurred:', event.rowIndex, event.colId);});Complete Accessibility Example
import { createGrid, announceToScreenReader } from '@zengrid/core';import type { Column, GridOptions } from '@zengrid/core';
// Column definitions with ARIA labelsconst columns: Column[] = [ { id: 'id', header: 'ID', width: 60, ariaLabel: 'User ID', }, { id: 'name', header: 'Name', width: 200, sortable: true, ariaLabel: 'User name', }, { id: 'email', header: 'Email', width: 250, sortable: true, ariaLabel: 'Email address', }, { id: 'status', header: 'β', width: 60, ariaLabel: 'Account status', }, { id: 'actions', header: '', width: 120, ariaLabel: 'Available actions', },];
// Grid options with accessibility enabledconst grid = createGrid(container, { columns, rowCount: 100, enableA11y: true, tabNavigationMode: 'cell',
// ARIA labels ariaLabel: 'User management grid', ariaDescription: 'Navigate with arrow keys, press Enter to edit',
data: (rowIndex, colId) => { // Data implementation return `Cell ${rowIndex}-${colId}`; },});
// Announce data loadannounceToScreenReader('100 users loaded');
// Focus managementgrid.on('focus:change', (event) => { if (event.cell) { const { rowIndex, colId } = event.cell; const value = grid.getCellValue(rowIndex, colId);
// Announce cell content announceToScreenReader( `${columns.find(c => c.id === colId)?.header}: ${value}` ); }});
// Sort announcementsgrid.on('sort:changed', (event) => { const sortState = event.sortState;
if (sortState.length === 0) { announceToScreenReader('Sort cleared'); } else { const { colId, direction } = sortState[0]; const column = columns.find(c => c.id === colId); announceToScreenReader( `Sorted by ${column?.header} ${direction === 'asc' ? 'ascending' : 'descending'}` ); }});
// Filter announcementsgrid.on('filter:changed', (event) => { const visibleCount = event.visibleRowCount; const totalCount = grid.getRowCount();
announceToScreenReader( `Filters applied, showing ${visibleCount} of ${totalCount} rows` );});
// Selection announcementsgrid.on('selection:changed', (event) => { const count = event.selectedRows.length;
if (count === 0) { announceToScreenReader('Selection cleared'); } else if (count === 1) { announceToScreenReader(`Row ${event.selectedRows[0] + 1} selected`); } else { announceToScreenReader(`${count} rows selected`); }});
// Edit announcementsgrid.on('edit:start', (event) => { const column = columns.find(c => c.id === event.colId); announceToScreenReader( `Editing ${column?.header} at row ${event.rowIndex + 1}` );});
grid.on('edit:commit', () => { announceToScreenReader('Changes saved');});
grid.on('edit:cancel', () => { announceToScreenReader('Edit cancelled');});
// Custom keyboard shortcutscontainer.addEventListener('keydown', (e) => { // Slash to focus quick filter if (e.key === '/') { e.preventDefault(); document.getElementById('quick-filter')?.focus(); announceToScreenReader('Quick filter focused'); }
// Question mark for keyboard shortcuts help if (e.key === '?') { e.preventDefault(); showKeyboardShortcutsDialog(); announceToScreenReader('Keyboard shortcuts dialog opened'); }});
// Keyboard shortcuts helpfunction showKeyboardShortcutsDialog() { const shortcuts = ` Arrow Keys: Navigate between cells Tab: Move to next cell Enter: Edit cell Escape: Cancel edit Home: First cell in row End: Last cell in row Ctrl+Home: First cell in grid Ctrl+End: Last cell in grid Page Up/Down: Scroll by page Ctrl+A: Select all /: Focus quick filter ?: Show this help `;
alert(shortcuts);}This example provides comprehensive accessibility support including ARIA labels, keyboard navigation, screen reader announcements, and focus management.
Testing Accessibility
Test accessibility with screen readers and keyboard-only navigation:
Screen Reader Testing
- NVDA (Windows): Free, open-source screen reader
- JAWS (Windows): Professional screen reader
- VoiceOver (macOS/iOS): Built-in Apple screen reader
- TalkBack (Android): Built-in Android screen reader
Keyboard-Only Testing
Navigate the grid using only the keyboard:
- Tab to grid container
- Use arrow keys to navigate cells
- Press Enter to edit
- Press Escape to cancel
- Use Home/End for row navigation
- Use Ctrl+Home/End for grid navigation
- Use Page Up/Down for page navigation
Automated Testing
import { test, expect } from '@playwright/test';
test('grid has proper ARIA attributes', async ({ page }) => { await page.goto('/grid-page');
// Check grid role const grid = page.locator('[role="grid"]'); await expect(grid).toBeVisible();
// Check row count await expect(grid).toHaveAttribute('aria-rowcount', '100');
// Check column count await expect(grid).toHaveAttribute('aria-colcount', '5');
// Check first cell const firstCell = page.locator('[role="gridcell"]').first(); await expect(firstCell).toHaveAttribute('aria-colindex', '1'); await expect(firstCell).toHaveAttribute('tabindex', '0');});
test('keyboard navigation works', async ({ page }) => { await page.goto('/grid-page');
// Focus first cell await page.keyboard.press('Tab');
// Navigate with arrows await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowRight');
// Check focused cell const focused = page.locator('[role="gridcell"]:focus'); await expect(focused).toBeVisible();});Best Practices
1. Always Enable Accessibility
Keep enableA11y: true unless you have a specific reason to disable it.
2. Provide ARIA Labels
Add ariaLabel to columns without visible text (icons, actions).
3. Announce State Changes
Use announceToScreenReader() for important state changes.
4. Support Keyboard Navigation
Ensure all functionality is accessible via keyboard.
5. Test with Screen Readers
Regularly test with actual screen readers to ensure usability.
6. Provide Skip Links
Add skip links to allow bypassing the grid:
<a href="#after-grid" class="skip-link">Skip to content after grid</a><div id="grid-container"></div><div id="after-grid">Content after grid</div>Next Steps
- Learn about keyboard shortcuts customization
- Explore screen reader optimization
- Read about WCAG compliance