Accessibility
Added in Handsontable 14.0.0Handsontable supports keyboard navigation, screen readers, and ARIA roles. Use these steps to configure accessible behavior for your users.
Handsontable targets WCAG 2.2 Level A and AA. The accessibility conformance report states how far the grid conforms today, per success criterion.
Overview
Accessibility features of Handsontable include:
- Keyboard navigation that lets you use the grid without a mouse.
- Support for screen readers, tested with VoiceOver (macOS and iOS) and NVDA (Windows).
- Flexible API to configure keyboard shortcuts and navigation methods.
- A published accessibility conformance report, version 1.1 (March 2026), with the result per success criterion and the known gaps.
Standards built on WCAG
Most local standards and regulations build on the Web Content Accessibility Guidelines (WCAG). Handsontable targets WCAG 2.2 Level A and AA. The accessibility conformance report, version 1.1 (March 2026), states, for each success criterion, the conformance level or that it was not evaluated, and lists the known gaps. The standards that build on WCAG include:
USA
Europe
Canada
Keyboard navigation
Handsontable doesn’t require a mouse to navigate across the grid’s elements. This is an important feature for those users with temporary or permanent motor impairments for whom following the mouse cursor is difficult. Keyboard navigation is also a great way to improve productivity, which is why many users choose the keyboard over the mouse regardless of their accessibility needs.
Our experience with hundreds of implementations shows that Handsontable tends to be used either as a spreadsheet application or a data grid component. While at first the difference seems subtle, it significantly impacts user expectations regarding navigation.
In a typical spreadsheet application (think of Microsoft Excel or Google Sheets), you can’t move the focus onto headers. This makes it difficult to sort or filter data without knowing complex keyboard shortcuts. Additionally, opening a column menu is not trivial.
Handsontable offers flexibility in this regard, letting you switch between data grid and spreadsheet “modes”. To do that switch, you can use a combination of two options: navigableHeaders to enable or disable moving focus onto headers, and tabNavigation to decide if the Tab key can be used to navigate across cells and headers.
To control arrow-key wrapping at grid boundaries, use autoWrapRow and autoWrapCol. autoWrapRow wraps focus to the adjacent row when you move right or left past a row edge. autoWrapCol wraps focus to the adjacent column when you move up or down past a column edge.
The following table provides more details about these two scenarios:
| Aspect | Data grid mode | Spreadsheet mode (default) |
|---|---|---|
| Configuration | navigableHeaders: true tabNavigation: false | navigableHeaders: false tabNavigation: true |
| Primary navigation method | Arrow keys | Tab / Shift+Tab |
| Navigable headers | Yes | No |
| Navigation | Use the arrow keys navigate across the grid. Use simple shortcuts such as Enter or Space to open menus or interact with headers, cells, or cell editors. You can’t use the Tab key for navigation. | Use Tab / Shift+Tab to navigate across the grid. This behavior is similar to Excel or Google Sheets. To open menus, use more complex shortcuts. |
| Focus behavior | One tab stop. The grid is included in the page tab sequence only once. | Multiple tab stops. All tabbable elements of the grid, such as cells, are included in the page tab sequence. |
Navigation shortcuts
Handsontable provides a wide range of keyboard shortcuts, but some of them are particularly important for users who navigate the grid with the keyboard only. For example, actions triggered while navigating across headers involve simple key combinations, making them intuitive and useful. For more complex scenarios, you can customize the shortcuts keys through the API.
| Windows | macOS | Action | Focused element |
|---|---|---|---|
| Shift+Alt+↓ | ⇧+⌥+↓ | Open a column menu | Any cell |
| Ctrl+Enter | ⌘+Enter | Open a column menu | Column header |
| Enter | Enter | Sort data | Column header |
| Alt+A | ⌥+A | Clear filters | Any cell |
| Ctrl+Space | ⌃+Space* | Select a column | Any cell |
| Shift+Space | ⇧+Space | Select a row | Any cell |
| Ctrl+Shift+\ Shift+F10 | ⌘+⇧+\ ⇧+F10 | Open a context menu | Any cell |
*To use this shortcut, disable the default macOS behavior for the Ctrl+Space key combination, under System Settings > Keyboard > Keyboard Shortcuts > Input Sources.
For filter-menu-specific navigation details, including Tab traversal and hover behavior, see Column filter.
Support for screen readers
Although semantic HTML doesn’t need any additional attributes to be properly interpreted by assistive technologies, some of Handsontable’s complex features are not fully covered by the HTML specification. That’s why Handsontable provides support for screen readers with ARIA attributes (Accessible Rich Internet Applications) applied to its HTML markup.
Handsontable is tested with the screen readers below. The list is the tested matrix of the accessibility conformance report; it changes only when an audit or an automated run adds to it.
- VoiceOver on macOS 15.3.2: external audit (Kinaole, December 2025 to January 2026).
- VoiceOver on iOS 18.3: external audit (Kinaole, December 2025 to January 2026); manual check once per major release.
- NVDA 2025.3 on Windows 11: external audit (Kinaole, December 2025 to January 2026).
Browsers in the last audit: Chrome 141 and Firefox 143.
VoiceOver on iOS was part of the last audit. Mobile screen readers are checked by hand once per major release.
Screen-reader checks are manual today. The automated nightly runs with NVDA and VoiceOver that are being set up join this list when they run.
Accessible data grid demo
Check out the interactive demo below to see how various Handsontable settings impact its accessibility level and affect the user experience.
import { useState } from 'react';import { HotTable, HotColumn } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';registerAllModules();/* start:skip-in-preview */const data = [ {461 collapsed lines
companyName: 'Hodkiewicz - Hintz', productName: 'Rustic Soft Ball', sellDate: '2023-07-05', inStock: false, qty: 82, orderId: '16-3974628', country: 'United Kingdom', }, { companyName: 'Rath LLC', productName: 'Small Frozen Tuna', sellDate: '2023-05-31', inStock: false, qty: 459, orderId: '77-7839351', country: 'Costa Rica', }, { companyName: 'Reichert LLC', productName: 'Rustic Soft Ball', sellDate: '2023-03-16', inStock: false, qty: 318, orderId: '75-6343150', country: 'United States of America', }, { companyName: 'Kozey Inc', productName: 'Sleek Wooden Bacon', sellDate: '2023-04-24', inStock: true, qty: 177, orderId: '56-3608689', country: 'Pitcairn Islands', }, { companyName: 'Nader - Fritsch', productName: 'Awesome Wooden Hat', sellDate: '2023-04-29', inStock: true, qty: 51, orderId: '58-1204318', country: 'Argentina', }, { companyName: 'Gerhold - Rowe', productName: 'Tasty Frozen Table', sellDate: '2023-03-27', inStock: false, qty: 439, orderId: '62-6066132', country: 'Senegal', }, { companyName: 'Rath LLC', productName: 'Awesome Wooden Hat', sellDate: '2022-11-24', inStock: false, qty: 493, orderId: '76-7785471', country: 'Cyprus', }, { companyName: 'Kozey Inc', productName: 'Rustic Soft Ball', sellDate: '2023-08-11', inStock: false, qty: 225, orderId: '34-3551159', country: 'Saint Martin', }, { companyName: 'Hodkiewicz - Hintz', productName: 'Awesome Wooden Hat', sellDate: '2023-02-07', inStock: false, qty: 261, orderId: '77-1112514', country: 'Chile', }, { companyName: 'Hegmann Inc', productName: 'Tasty Frozen Table', sellDate: '2023-05-06', inStock: false, qty: 439, orderId: '12-3252385', country: 'Switzerland', }, { companyName: 'Weber Inc', productName: 'Awesome Wooden Hat', sellDate: '2023-04-22', inStock: true, qty: 235, orderId: '71-7639998', country: 'Brazil', }, { companyName: 'Jacobi - Kutch', productName: 'Sleek Wooden Bacon', sellDate: '2022-12-13', inStock: true, qty: 163, orderId: '68-1588829', country: 'Burkina Faso', }, { companyName: 'Jenkins LLC', productName: 'Small Rubber Shoes', sellDate: '2023-03-26', inStock: true, qty: 8, orderId: '61-6324553', country: 'Virgin Islands, U.S.', }, { companyName: 'Koepp and Sons', productName: 'Sleek Wooden Bacon', sellDate: '2023-05-04', inStock: true, qty: 355, orderId: '74-6985005', country: 'Mozambique', }, { companyName: 'Doyle Group', productName: 'Awesome Wooden Hat', sellDate: '2023-08-01', inStock: false, qty: 186, orderId: '84-4370131', country: 'Cocos (Keeling) Islands', }, { companyName: 'Rempel - Durgan', productName: 'Tasty Frozen Table', sellDate: '2023-09-30', inStock: false, qty: 284, orderId: '13-6461825', country: 'Monaco', }, { companyName: 'Lesch - Jakubowski', productName: 'Small Fresh Bacon', sellDate: '2023-09-26', inStock: true, qty: 492, orderId: '13-9465439', country: 'Iran', }, { companyName: 'Jacobi - Kutch', productName: 'Rustic Cotton Ball', sellDate: '2023-05-04', inStock: true, qty: 300, orderId: '76-5194058', country: 'Indonesia', }, { companyName: 'Gerhold - Rowe', productName: 'Rustic Cotton Ball', sellDate: '2023-07-07', inStock: true, qty: 493, orderId: '61-8600792', country: 'Norfolk Island', }, { companyName: 'Johnston - Wisozk', productName: 'Small Fresh Fish', sellDate: '2023-07-14', inStock: false, qty: 304, orderId: '10-6007287', country: 'Romania', }, { companyName: 'Gutkowski Inc', productName: 'Small Fresh Bacon', sellDate: '2023-01-10', inStock: true, qty: 375, orderId: '25-1164132', country: 'Afghanistan', }, { companyName: 'Koepp and Sons', productName: 'Small Fresh Fish', sellDate: '2023-03-30', inStock: false, qty: 365, orderId: '75-7975820', country: 'Germany', }, { companyName: 'Zboncak and Sons', productName: 'Small Fresh Fish', sellDate: '2023-08-17', inStock: false, qty: 308, orderId: '59-6251875', country: 'Tajikistan', }, { companyName: 'Mills Group', productName: 'Rustic Soft Ball', sellDate: '2023-09-30', inStock: false, qty: 191, orderId: '67-7521441', country: 'Puerto Rico', }, { companyName: 'Zboncak and Sons', productName: 'Awesome Wooden Hat', sellDate: '2023-03-18', inStock: false, qty: 208, orderId: '19-4264192', country: 'Bolivia', }, { companyName: 'Rath LLC', productName: 'Rustic Soft Ball', sellDate: '2023-06-14', inStock: true, qty: 191, orderId: '78-5742060', country: 'Benin', }, { companyName: 'Upton - Reichert', productName: 'Tasty Frozen Table', sellDate: '2023-02-27', inStock: false, qty: 45, orderId: '26-6191298', country: 'Tunisia', }, { companyName: 'Carroll Group', productName: 'Rustic Soft Ball', sellDate: '2022-12-12', inStock: true, qty: 385, orderId: '13-7828353', country: 'Switzerland', }, { companyName: 'Reichel Group', productName: 'Small Frozen Tuna', sellDate: '2022-12-12', inStock: true, qty: 117, orderId: '67-9643738', country: 'Mongolia', }, { companyName: 'Kozey Inc', productName: 'Rustic Soft Ball', sellDate: '2023-03-24', inStock: false, qty: 335, orderId: '78-1331653', country: 'Angola', }, { companyName: 'Brown LLC', productName: 'Small Rubber Shoes', sellDate: '2023-06-13', inStock: true, qty: 305, orderId: '63-2315723', country: 'Switzerland', }, { companyName: 'Weber Inc', productName: 'Rustic Cotton Ball', sellDate: '2023-09-07', inStock: true, qty: 409, orderId: '53-6782557', country: 'Indonesia', }, { companyName: 'OReilly LLC', productName: 'Tasty Frozen Table', sellDate: '2023-05-18', inStock: true, qty: 318, orderId: '91-7787675', country: 'Mayotte', }, { companyName: 'Weber Inc', productName: 'Sleek Wooden Bacon', sellDate: '2023-04-20', inStock: false, qty: 234, orderId: '41-3560672', country: 'Switzerland', }, { companyName: 'Hodkiewicz Inc', productName: 'Tasty Frozen Table', sellDate: '2023-10-19', inStock: true, qty: 136, orderId: '48-6028776', country: 'Peru', }, { companyName: 'Lesch and Sons', productName: 'Rustic Cotton Ball', sellDate: '2023-09-29', inStock: false, qty: 187, orderId: '84-3770456', country: 'Central African Republic', }, { companyName: 'Pouros - Brakus', productName: 'Small Frozen Tuna', sellDate: '2023-01-29', inStock: false, qty: 350, orderId: '08-4844950', country: 'Isle of Man', }, { companyName: 'Batz - Rice', productName: 'Small Rubber Shoes', sellDate: '2023-11-06', inStock: false, qty: 252, orderId: '88-4899852', country: 'Burundi', }, { companyName: 'Kub Inc', productName: 'Small Fresh Fish', sellDate: '2023-09-05', inStock: true, qty: 306, orderId: '06-5022461', country: 'Mauritius', }, { companyName: 'Hills and Sons', productName: 'Small Frozen Tuna', sellDate: '2023-11-07', inStock: false, qty: 435, orderId: '99-5539911', country: 'Somalia', }, { companyName: 'Shanahan - Boyle', productName: 'Small Frozen Tuna', sellDate: '2023-06-19', inStock: true, qty: 171, orderId: '82-8162453', country: 'Virgin Islands, U.S.', }, { companyName: 'Luettgen Inc', productName: 'Awesome Wooden Hat', sellDate: '2023-09-30', inStock: false, qty: 6, orderId: '02-8118250', country: 'Colombia', }, { companyName: 'Hegmann Inc', productName: 'Small Rubber Shoes', sellDate: '2023-02-16', inStock: true, qty: 278, orderId: '07-9773343', country: 'Central African Republic', }, { companyName: 'Kub Inc', productName: 'Small Frozen Tuna', sellDate: '2023-08-08', inStock: false, qty: 264, orderId: '66-4470479', country: 'Norfolk Island', }, { companyName: 'Kub Inc', productName: 'Tasty Frozen Table', sellDate: '2023-06-06', inStock: true, qty: 494, orderId: '13-1175339', country: 'Liechtenstein', }, { companyName: 'Hahn - Welch', productName: 'Small Frozen Tuna', sellDate: '2023-06-12', inStock: false, qty: 485, orderId: '32-9127309', country: 'Bahrain', }, { companyName: 'Nader - Fritsch', productName: 'Small Frozen Tuna', sellDate: '2023-04-08', inStock: true, qty: 332, orderId: '41-3774568', country: 'Montserrat', }, { companyName: 'Crona and Sons', productName: 'Small Fresh Bacon', sellDate: '2023-06-21', inStock: true, qty: 104, orderId: '48-9995090', country: 'Syrian Arab Republic', }, { companyName: 'Lind Group', productName: 'Rustic Cotton Ball', sellDate: '2023-08-17', inStock: false, qty: 51, orderId: '68-9599400', country: 'Czech Republic', }, { companyName: 'Labadie LLC', productName: 'Small Fresh Bacon', sellDate: '2023-04-20', inStock: true, qty: 155, orderId: '52-4334332', country: 'Croatia', }, { companyName: 'Doyle Group', productName: 'Sleek Wooden Bacon', sellDate: '2023-07-23', inStock: false, qty: 465, orderId: '63-8894526', country: 'Indonesia', },];const countries = data.reduce((acc, curr) => { if (acc.includes(curr.country)) { return acc; } return [...acc, curr.country];}, []);/* end:skip-in-preview */// Handsontable optionsconst hotOptions = { data, height: 464, colWidths: [160, 165, 130, 120, 100, 110, 216], autoRowSize: true, colHeaders: ['Company name', 'Product name', 'Sell date', 'In stock', 'Qty', 'Order ID', 'Country'], dropdownMenu: true, hiddenColumns: { indicators: true, }, multiColumnSorting: true, filters: true, rowHeaders: true, manualRowMove: true, licenseKey: 'non-commercial-and-evaluation',};const ExampleComponent = () => { const [toggleableOptions, setToggleableOptions] = useState({ tabNavigation: true, navigableHeaders: true, renderAllRows: false, renderAllColumns: false, enterBeginsEditing: true, autoWrapRow: true, autoWrapCol: true, headerClassName: 'htLeft', enterMoves: { col: 0, row: 1 }, }); return (<div className="example-container"> {/* DemoOptions component for changing Handsontable options */} <DemoOptions changeToggleOptions={setToggleableOptions} {...toggleableOptions}/>
<input className="placeholder-input" type="text" placeholder="Focusable text input"/>
{/* Handsontable component with dynamic options */} <HotTable // Handsontable needs to reload when changing virtualization // by changing the key, we force the component to reload key={String(toggleableOptions.renderAllRows)} {...hotOptions} // Pass in the options which can change for demo {...toggleableOptions}> {/* Define HotColumns for the data */} <HotColumn data="companyName" type="text"/> <HotColumn data="productName" type="text"/> <HotColumn data="sellDate" locale="en-GB" dateFormat={{ day: '2-digit', month: '2-digit', year: 'numeric' }} type="intl-date"/> <HotColumn data="inStock" type="checkbox" className="htCenter" headerClassName="htCenter"/> <HotColumn data="qty" type="numeric" headerClassName="htRight"/> <HotColumn data="orderId" type="text"/> <HotColumn data="country" type="dropdown" source={countries}/> </HotTable> <input className="placeholder-input" type="text" placeholder="Focusable text input"/> </div>);};// Demo Options allows you to change the Handsontable options// This allows us to change the Handsontable settings from the UI, showcasing// the flexibility of Handsontable in configuring according to your needs.function DemoOptions({ tabNavigation, navigableHeaders, renderAllRows, renderAllColumns, enterBeginsEditing, autoWrapRow, autoWrapCol, enterMoves, changeToggleOptions, }) { // on checkbox change, update handsontable option const handleCheckboxChange = (checkboxName) => { switch (checkboxName) { case 'enable-tab-navigation': changeToggleOptions((existing) => ({ ...existing, tabNavigation: !tabNavigation, })); break; case 'enable-header-navigation': changeToggleOptions((existing) => ({ ...existing, navigableHeaders: !navigableHeaders, })); break; case 'enable-cell-virtualization': changeToggleOptions((existing) => ({ ...existing, renderAllRows: !renderAllRows, renderAllColumns: !renderAllColumns, })); break; case 'enable-cell-enter-editing': changeToggleOptions((existing) => ({ ...existing, enterBeginsEditing: !enterBeginsEditing, })); break; case 'enable-arrow-rl-first-last-column': changeToggleOptions((existing) => ({ ...existing, autoWrapRow: !autoWrapRow, })); break; case 'enable-arrow-td-first-last-column': changeToggleOptions((existing) => ({ ...existing, autoWrapCol: !autoWrapCol, })); break; case 'enable-enter-focus-editing': changeToggleOptions((existing) => ({ ...existing, enterMoves: enterMoves.row !== 1 ? { col: 0, row: 1 } : { col: 0, row: 0 }, })); break; default: break; } }; return (<> <div className="checkbox-container"> <div> <label className="option-label" htmlFor="enable-tab-navigation" id="tab-navigation-label"> <input checked={tabNavigation} type="checkbox" id="enable-tab-navigation" name="enable-tab-navigation" aria-label="Enable navigation with the Tab key" onChange={() => handleCheckboxChange('enable-tab-navigation')}/> Enable navigation with the Tab key </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#tabnavigation" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more enabling/disabling tab navigation (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-header-navigation" id="header-navigation-label"> <input checked={navigableHeaders} type="checkbox" id="enable-header-navigation" name="enable-header-navigation" aria-labelledby="header-navigation-label" onChange={() => handleCheckboxChange('enable-header-navigation')}/> Enable navigation across headers </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#navigableheaders" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about enabling/disabling tab navigation across headers (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-cell-virtualization" id="cell-virtualization-label"> <input checked={!renderAllRows} type="checkbox" id="enable-cell-virtualization" name="enable-cell-virtualization" aria-labelledby="cell-virtualization-label" onChange={() => handleCheckboxChange('enable-cell-virtualization')}/> Enable cells virtualization </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#renderAllRows" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about row virtualization (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-cell-enter-editing" id="cell-enter-editing-label"> <input checked={enterBeginsEditing} type="checkbox" id="enable-cell-enter-editing" name="enable-cell-enter-editing" aria-labelledby="cell-enter-editing-label" onChange={() => handleCheckboxChange('enable-cell-enter-editing')}/> The Enter key begins cell editing </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#enterbeginsediting" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about Enter key cell editing (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-arrow-rl-first-last-column" id="arrow-rl-first-last-column-label"> <input checked={autoWrapRow} type="checkbox" id="enable-arrow-rl-first-last-column" name="enableArrowFirstLastColumn" aria-labelledby="arrow-rl-first-last-column-label" onChange={() => handleCheckboxChange('enable-arrow-rl-first-last-column')}/> The right/left arrow keys wrap focus to the adjacent row </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#autowraprow" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about right/left arrow key behavior (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-arrow-td-first-last-column" id="arrow-td-first-last-column-label"> <input checked={autoWrapCol} type="checkbox" id="enable-arrow-td-first-last-column" name="enable-arrow-td-first-last-column" aria-labelledby="arrow-td-first-last-column-label" onChange={() => handleCheckboxChange('enable-arrow-td-first-last-column')}/> The up/down arrow keys wrap focus to the adjacent column </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#autowrapcol" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about up/down arrow key behavior (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-enter-focus-editing" id="enter-focus-editing-label"> <input checked={enterMoves.row !== 0} type="checkbox" id="enable-enter-focus-editing" name="enable-enter-focus-editing" aria-labelledby="enter-focus-editing-label" onChange={() => handleCheckboxChange('enable-enter-focus-editing')}/> The Enter key moves the focus after cell edition </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#entermoves" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about Enter key focus behavior (opens in a new window)"> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"/> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"/> </svg> </a> </div> </div> </>);}export default ExampleComponent;import { useState } from 'react';import { HotTable, HotColumn } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';
registerAllModules();
interface Toggle { tabNavigation: boolean; navigableHeaders: boolean; renderAllRows: boolean; renderAllColumns: boolean; enterBeginsEditing: boolean; autoWrapRow: boolean; autoWrapCol: boolean; enterMoves: { col: number; row: number }; changeToggleOptions: any;}
interface Product { companyName: string; productName: string; sellDate: string; inStock: boolean; qty: number; orderId: string; country: string;}
/* start:skip-in-preview */461 collapsed lines
const data: Product[] = [ { companyName: 'Hodkiewicz - Hintz', productName: 'Rustic Soft Ball', sellDate: '2023-07-05', inStock: false, qty: 82, orderId: '16-3974628', country: 'United Kingdom', }, { companyName: 'Rath LLC', productName: 'Small Frozen Tuna', sellDate: '2023-05-31', inStock: false, qty: 459, orderId: '77-7839351', country: 'Costa Rica', }, { companyName: 'Reichert LLC', productName: 'Rustic Soft Ball', sellDate: '2023-03-16', inStock: false, qty: 318, orderId: '75-6343150', country: 'United States of America', }, { companyName: 'Kozey Inc', productName: 'Sleek Wooden Bacon', sellDate: '2023-04-24', inStock: true, qty: 177, orderId: '56-3608689', country: 'Pitcairn Islands', }, { companyName: 'Nader - Fritsch', productName: 'Awesome Wooden Hat', sellDate: '2023-04-29', inStock: true, qty: 51, orderId: '58-1204318', country: 'Argentina', }, { companyName: 'Gerhold - Rowe', productName: 'Tasty Frozen Table', sellDate: '2023-03-27', inStock: false, qty: 439, orderId: '62-6066132', country: 'Senegal', }, { companyName: 'Rath LLC', productName: 'Awesome Wooden Hat', sellDate: '2022-11-24', inStock: false, qty: 493, orderId: '76-7785471', country: 'Cyprus', }, { companyName: 'Kozey Inc', productName: 'Rustic Soft Ball', sellDate: '2023-08-11', inStock: false, qty: 225, orderId: '34-3551159', country: 'Saint Martin', }, { companyName: 'Hodkiewicz - Hintz', productName: 'Awesome Wooden Hat', sellDate: '2023-02-07', inStock: false, qty: 261, orderId: '77-1112514', country: 'Chile', }, { companyName: 'Hegmann Inc', productName: 'Tasty Frozen Table', sellDate: '2023-05-06', inStock: false, qty: 439, orderId: '12-3252385', country: 'Switzerland', }, { companyName: 'Weber Inc', productName: 'Awesome Wooden Hat', sellDate: '2023-04-22', inStock: true, qty: 235, orderId: '71-7639998', country: 'Brazil', }, { companyName: 'Jacobi - Kutch', productName: 'Sleek Wooden Bacon', sellDate: '2022-12-13', inStock: true, qty: 163, orderId: '68-1588829', country: 'Burkina Faso', }, { companyName: 'Jenkins LLC', productName: 'Small Rubber Shoes', sellDate: '2023-03-26', inStock: true, qty: 8, orderId: '61-6324553', country: 'Virgin Islands, U.S.', }, { companyName: 'Koepp and Sons', productName: 'Sleek Wooden Bacon', sellDate: '2023-05-04', inStock: true, qty: 355, orderId: '74-6985005', country: 'Mozambique', }, { companyName: 'Doyle Group', productName: 'Awesome Wooden Hat', sellDate: '2023-08-01', inStock: false, qty: 186, orderId: '84-4370131', country: 'Cocos (Keeling) Islands', }, { companyName: 'Rempel - Durgan', productName: 'Tasty Frozen Table', sellDate: '2023-09-30', inStock: false, qty: 284, orderId: '13-6461825', country: 'Monaco', }, { companyName: 'Lesch - Jakubowski', productName: 'Small Fresh Bacon', sellDate: '2023-09-26', inStock: true, qty: 492, orderId: '13-9465439', country: 'Iran', }, { companyName: 'Jacobi - Kutch', productName: 'Rustic Cotton Ball', sellDate: '2023-05-04', inStock: true, qty: 300, orderId: '76-5194058', country: 'Indonesia', }, { companyName: 'Gerhold - Rowe', productName: 'Rustic Cotton Ball', sellDate: '2023-07-07', inStock: true, qty: 493, orderId: '61-8600792', country: 'Norfolk Island', }, { companyName: 'Johnston - Wisozk', productName: 'Small Fresh Fish', sellDate: '2023-07-14', inStock: false, qty: 304, orderId: '10-6007287', country: 'Romania', }, { companyName: 'Gutkowski Inc', productName: 'Small Fresh Bacon', sellDate: '2023-01-10', inStock: true, qty: 375, orderId: '25-1164132', country: 'Afghanistan', }, { companyName: 'Koepp and Sons', productName: 'Small Fresh Fish', sellDate: '2023-03-30', inStock: false, qty: 365, orderId: '75-7975820', country: 'Germany', }, { companyName: 'Zboncak and Sons', productName: 'Small Fresh Fish', sellDate: '2023-08-17', inStock: false, qty: 308, orderId: '59-6251875', country: 'Tajikistan', }, { companyName: 'Mills Group', productName: 'Rustic Soft Ball', sellDate: '2023-09-30', inStock: false, qty: 191, orderId: '67-7521441', country: 'Puerto Rico', }, { companyName: 'Zboncak and Sons', productName: 'Awesome Wooden Hat', sellDate: '2023-03-18', inStock: false, qty: 208, orderId: '19-4264192', country: 'Bolivia', }, { companyName: 'Rath LLC', productName: 'Rustic Soft Ball', sellDate: '2023-06-14', inStock: true, qty: 191, orderId: '78-5742060', country: 'Benin', }, { companyName: 'Upton - Reichert', productName: 'Tasty Frozen Table', sellDate: '2023-02-27', inStock: false, qty: 45, orderId: '26-6191298', country: 'Tunisia', }, { companyName: 'Carroll Group', productName: 'Rustic Soft Ball', sellDate: '2022-12-12', inStock: true, qty: 385, orderId: '13-7828353', country: 'Switzerland', }, { companyName: 'Reichel Group', productName: 'Small Frozen Tuna', sellDate: '2022-12-12', inStock: true, qty: 117, orderId: '67-9643738', country: 'Mongolia', }, { companyName: 'Kozey Inc', productName: 'Rustic Soft Ball', sellDate: '2023-03-24', inStock: false, qty: 335, orderId: '78-1331653', country: 'Angola', }, { companyName: 'Brown LLC', productName: 'Small Rubber Shoes', sellDate: '2023-06-13', inStock: true, qty: 305, orderId: '63-2315723', country: 'Switzerland', }, { companyName: 'Weber Inc', productName: 'Rustic Cotton Ball', sellDate: '2023-09-07', inStock: true, qty: 409, orderId: '53-6782557', country: 'Indonesia', }, { companyName: 'OReilly LLC', productName: 'Tasty Frozen Table', sellDate: '2023-05-18', inStock: true, qty: 318, orderId: '91-7787675', country: 'Mayotte', }, { companyName: 'Weber Inc', productName: 'Sleek Wooden Bacon', sellDate: '2023-04-20', inStock: false, qty: 234, orderId: '41-3560672', country: 'Switzerland', }, { companyName: 'Hodkiewicz Inc', productName: 'Tasty Frozen Table', sellDate: '2023-10-19', inStock: true, qty: 136, orderId: '48-6028776', country: 'Peru', }, { companyName: 'Lesch and Sons', productName: 'Rustic Cotton Ball', sellDate: '2023-09-29', inStock: false, qty: 187, orderId: '84-3770456', country: 'Central African Republic', }, { companyName: 'Pouros - Brakus', productName: 'Small Frozen Tuna', sellDate: '2023-01-29', inStock: false, qty: 350, orderId: '08-4844950', country: 'Isle of Man', }, { companyName: 'Batz - Rice', productName: 'Small Rubber Shoes', sellDate: '2023-11-06', inStock: false, qty: 252, orderId: '88-4899852', country: 'Burundi', }, { companyName: 'Kub Inc', productName: 'Small Fresh Fish', sellDate: '2023-09-05', inStock: true, qty: 306, orderId: '06-5022461', country: 'Mauritius', }, { companyName: 'Hills and Sons', productName: 'Small Frozen Tuna', sellDate: '2023-11-07', inStock: false, qty: 435, orderId: '99-5539911', country: 'Somalia', }, { companyName: 'Shanahan - Boyle', productName: 'Small Frozen Tuna', sellDate: '2023-06-19', inStock: true, qty: 171, orderId: '82-8162453', country: 'Virgin Islands, U.S.', }, { companyName: 'Luettgen Inc', productName: 'Awesome Wooden Hat', sellDate: '2023-09-30', inStock: false, qty: 6, orderId: '02-8118250', country: 'Colombia', }, { companyName: 'Hegmann Inc', productName: 'Small Rubber Shoes', sellDate: '2023-02-16', inStock: true, qty: 278, orderId: '07-9773343', country: 'Central African Republic', }, { companyName: 'Kub Inc', productName: 'Small Frozen Tuna', sellDate: '2023-08-08', inStock: false, qty: 264, orderId: '66-4470479', country: 'Norfolk Island', }, { companyName: 'Kub Inc', productName: 'Tasty Frozen Table', sellDate: '2023-06-06', inStock: true, qty: 494, orderId: '13-1175339', country: 'Liechtenstein', }, { companyName: 'Hahn - Welch', productName: 'Small Frozen Tuna', sellDate: '2023-06-12', inStock: false, qty: 485, orderId: '32-9127309', country: 'Bahrain', }, { companyName: 'Nader - Fritsch', productName: 'Small Frozen Tuna', sellDate: '2023-04-08', inStock: true, qty: 332, orderId: '41-3774568', country: 'Montserrat', }, { companyName: 'Crona and Sons', productName: 'Small Fresh Bacon', sellDate: '2023-06-21', inStock: true, qty: 104, orderId: '48-9995090', country: 'Syrian Arab Republic', }, { companyName: 'Lind Group', productName: 'Rustic Cotton Ball', sellDate: '2023-08-17', inStock: false, qty: 51, orderId: '68-9599400', country: 'Czech Republic', }, { companyName: 'Labadie LLC', productName: 'Small Fresh Bacon', sellDate: '2023-04-20', inStock: true, qty: 155, orderId: '52-4334332', country: 'Croatia', }, { companyName: 'Doyle Group', productName: 'Sleek Wooden Bacon', sellDate: '2023-07-23', inStock: false, qty: 465, orderId: '63-8894526', country: 'Indonesia', },];
const countries = data.reduce<string[]>((acc, curr) => { if (acc.includes(curr.country)) { return acc; }
return [...acc, curr.country];}, []);
/* end:skip-in-preview */
// Handsontable optionsconst hotOptions = { data, height: 464, colWidths: [160, 165, 130, 120, 100, 110, 216], autoRowSize: true, colHeaders: ['Company name', 'Product name', 'Sell date', 'In stock', 'Qty', 'Order ID', 'Country'], dropdownMenu: true, hiddenColumns: { indicators: true, }, multiColumnSorting: true, filters: true, rowHeaders: true, manualRowMove: true, licenseKey: 'non-commercial-and-evaluation',};
const ExampleComponent = () => { const [toggleableOptions, setToggleableOptions] = useState<Omit<Toggle, 'changeToggleOptions'>>({ tabNavigation: true, navigableHeaders: true, renderAllRows: false, renderAllColumns: false, enterBeginsEditing: true, autoWrapRow: true, autoWrapCol: true, headerClassName: 'htLeft', enterMoves: { col: 0, row: 1 }, });
return ( <div className="example-container"> {/* DemoOptions component for changing Handsontable options */} <DemoOptions changeToggleOptions={setToggleableOptions} {...toggleableOptions} />
<input className="placeholder-input" type="text" placeholder="Focusable text input" />
{/* Handsontable component with dynamic options */} <HotTable // Handsontable needs to reload when changing virtualization // by changing the key, we force the component to reload key={String(toggleableOptions.renderAllRows)} {...hotOptions} // Pass in the options which can change for demo {...toggleableOptions} > {/* Define HotColumns for the data */} <HotColumn data="companyName" type="text" /> <HotColumn data="productName" type="text" /> <HotColumn data="sellDate" locale="en-GB" dateFormat={{ day: '2-digit', month: '2-digit', year: 'numeric' }} type="intl-date" /> <HotColumn data="inStock" type="checkbox" className="htCenter" headerClassName="htCenter" /> <HotColumn data="qty" type="numeric" headerClassName="htRight" /> <HotColumn data="orderId" type="text" /> <HotColumn data="country" type="dropdown" source={countries} /> </HotTable> <input className="placeholder-input" type="text" placeholder="Focusable text input" /> </div> );};
// Demo Options allows you to change the Handsontable options// This allows us to change the Handsontable settings from the UI, showcasing// the flexibility of Handsontable in configuring according to your needs.function DemoOptions({ tabNavigation, navigableHeaders, renderAllRows, renderAllColumns, enterBeginsEditing, autoWrapRow, autoWrapCol, enterMoves, changeToggleOptions,}: Toggle) { // on checkbox change, update handsontable option const handleCheckboxChange = (checkboxName: string) => { switch (checkboxName) { case 'enable-tab-navigation': changeToggleOptions((existing: any) => ({ ...existing, tabNavigation: !tabNavigation, }));
break; case 'enable-header-navigation': changeToggleOptions((existing: any) => ({ ...existing, navigableHeaders: !navigableHeaders, }));
break; case 'enable-cell-virtualization': changeToggleOptions((existing: any) => ({ ...existing, renderAllRows: !renderAllRows, renderAllColumns: !renderAllColumns, }));
break; case 'enable-cell-enter-editing': changeToggleOptions((existing: any) => ({ ...existing, enterBeginsEditing: !enterBeginsEditing, }));
break; case 'enable-arrow-rl-first-last-column': changeToggleOptions((existing: any) => ({ ...existing, autoWrapRow: !autoWrapRow, }));
break; case 'enable-arrow-td-first-last-column': changeToggleOptions((existing: any) => ({ ...existing, autoWrapCol: !autoWrapCol, }));
break; case 'enable-enter-focus-editing': changeToggleOptions((existing: any) => ({ ...existing, enterMoves: enterMoves.row !== 1 ? { col: 0, row: 1 } : { col: 0, row: 0 }, }));
break; default: break; } };
return ( <> <div className="checkbox-container"> <div> <label className="option-label" htmlFor="enable-tab-navigation" id="tab-navigation-label"> <input checked={tabNavigation} type="checkbox" id="enable-tab-navigation" name="enable-tab-navigation" aria-label="Enable navigation with the Tab key" onChange={() => handleCheckboxChange('enable-tab-navigation')} /> Enable navigation with the Tab key </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#tabnavigation" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more enabling/disabling tab navigation (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-header-navigation" id="header-navigation-label"> <input checked={navigableHeaders} type="checkbox" id="enable-header-navigation" name="enable-header-navigation" aria-labelledby="header-navigation-label" onChange={() => handleCheckboxChange('enable-header-navigation')} /> Enable navigation across headers </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#navigableheaders" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about enabling/disabling tab navigation across headers (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-cell-virtualization" id="cell-virtualization-label"> <input checked={!renderAllRows} type="checkbox" id="enable-cell-virtualization" name="enable-cell-virtualization" aria-labelledby="cell-virtualization-label" onChange={() => handleCheckboxChange('enable-cell-virtualization')} /> Enable cells virtualization </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#renderAllRows" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about row virtualization (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-cell-enter-editing" id="cell-enter-editing-label"> <input checked={enterBeginsEditing} type="checkbox" id="enable-cell-enter-editing" name="enable-cell-enter-editing" aria-labelledby="cell-enter-editing-label" onChange={() => handleCheckboxChange('enable-cell-enter-editing')} /> The Enter key begins cell editing </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#enterbeginsediting" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about Enter key cell editing (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-arrow-rl-first-last-column" id="arrow-rl-first-last-column-label"> <input checked={autoWrapRow} type="checkbox" id="enable-arrow-rl-first-last-column" name="enableArrowFirstLastColumn" aria-labelledby="arrow-rl-first-last-column-label" onChange={() => handleCheckboxChange('enable-arrow-rl-first-last-column')} /> The right/left arrow keys wrap focus to the adjacent row </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#autowraprow" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about right/left arrow key behavior (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-arrow-td-first-last-column" id="arrow-td-first-last-column-label"> <input checked={autoWrapCol} type="checkbox" id="enable-arrow-td-first-last-column" name="enable-arrow-td-first-last-column" aria-labelledby="arrow-td-first-last-column-label" onChange={() => handleCheckboxChange('enable-arrow-td-first-last-column')} /> The up/down arrow keys wrap focus to the adjacent column </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#autowrapcol" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about up/down arrow key behavior (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> <div> <label className="option-label" htmlFor="enable-enter-focus-editing" id="enter-focus-editing-label"> <input checked={enterMoves.row !== 0} type="checkbox" id="enable-enter-focus-editing" name="enable-enter-focus-editing" aria-labelledby="enter-focus-editing-label" onChange={() => handleCheckboxChange('enable-enter-focus-editing')} /> The Enter key moves the focus after cell edition </label> <a href="https://handsontable.com/docs/react-data-grid/api/options/#entermoves" target="_blank" className="external-link" rel="noopener noreferrer" aria-label="Learn more about Enter key focus behavior (opens in a new window)" > <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" className="icon outbound"> <path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z" /> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9" /> </svg> </a> </div> </div> </> );}
export default ExampleComponent;.checkbox-container { display: flex; flex-direction: column; gap: 0.5rem; margin: 0 -1rem 0 !important; padding: 0 1rem 0.75rem; border-bottom: 1px solid var(--sl-color-gray-5, #e0e0e0);}
.checkbox-container > div { display: flex;}
.checkbox-container > div > label { display: flex; align-items: center; gap: 0.4rem;}
.external-link { margin-left: 0.5rem; position: relative; top: 2px; color: var(--sl-color-text, #333333);}
.external-link:hover { color: var(--sl-color-accent, #1A42E8);}
.placeholder-input { max-width: 20rem; padding: 0.4rem 0.625rem; font-size: var(--sl-text-sm, 0.875rem); line-height: 1.25rem; color: var(--sl-color-text, #333333); background: none; border: 1px solid var(--sl-color-gray-5, #e0e0e0); border-radius: 0; outline: none;}
.placeholder-input::placeholder { color: var(--sl-color-gray-3, #777777);}
.placeholder-input:focus { border-color: var(--sl-color-accent, #1A42E8);}
.option-label { color: var(--sl-color-text, #333333); font-size: var(--sl-text-sm, 0.875rem);}
/* We want the focus to be around input and label, in order to achieve this, we remove focus from the input and add it to the label (wrapper in this case) we then use the :focus-within pseudo class plus native focus styles https://css-tricks.com/copy-the-browsers-native-focus-styles/*/.option-label:focus-within { outline: 5px auto Highlight; outline: 5px auto -webkit-focus-ring-color;}
.option-label > input:focus { outline: none;}
.example-container { gap: 1rem; display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;}Disabling DOM virtualization for improved accessibility
By default, Handsontable uses DOM virtualization to display only the rows and columns that are currently visible on the screen, plus a few extra cells outside the visible area to ensure a seamless scrolling experience.
However, assistive technologies rely on the elements within the DOM appearing in the correct order.
Otherwise, they require the use of additional ARIA attributes,
such as row-colindex or aria-rowindex, to understand the grid’s structure and accurately announce (read) it to the user.
Handsontable describes hidden columns or rows, merged cells, and the sort order of a column (aria-sort) with ARIA attributes.
Screen readers do not always read them: a change of the sort order is not announced, and in some scenarios a screen reader announces incorrect indices or omits the attributes. See Known limitations.
To address this issue, we recommend disabling DOM virtualization, which entails loading all grid elements into the browser.
This action creates a complete Accessibility tree that can be easily parsed
and interpreted by assistive technology.
<HotTable // disable column virtualization renderAllColumns={true} // disable row virtualization renderAllRows={true}/>High-contrast theme
The recommended minimum contrast ratio for text against images or backgrounds is 4.5:1. To achieve this level of contrast with Handsontable’s default theme, you can:
- Override the grid’s CSS with your own styles.
- Use third-party software, such as the High Contrast extension for Chrome, supported by Google.
Requirements for developers
When you customize Handsontable, it’s you who’s responsible for ensuring the accessibility of your solution. Especially when you create a custom cell type or a custom plugin, remember to make them accessible to everyone.
Our recommendations for custom development:
- Test your code against the WCAG 2.2 Level A and AA success criteria frequently.
- Use proper color contrast, font size, and semantic HTML.
- If needed, implement additional WAI-ARIA attributes.
- Avoid flashing or blinking content.
- Test your customizations with real users who have different types of disabilities. If you don’t have access to real users, try Funkify, a disability simulator, which can help you look at your application from the perspective of users with different disabilities.
Maintaining accessibility
Handsontable keeps the grid accessible with the following measures:
- An automated axe-core scan with the WCAG 2.2 Level A and AA rule tags runs in CI on every change to the grid, on the three built-in themes, and reports every new finding.
- Keyboard-only end-to-end tests cover the keyboard path of the grid’s controls, and every change to a control adds one that also checks its accessible name.
- The accessibility conformance report states the result per success criterion. Version 1.2 of this report is in preparation: every Level A and AA criterion judged, the evaluated product version stated, and the international edition of the VPAT template.
- Screen-reader testing with the matrix listed under Support for screen readers.
Known limitations
The accessibility conformance report lists the ones that carry a reference and a success criterion; the rest come from Handsontable’s own testing.
- Sorting state changes are not announced to screen readers (WCAG 4.1.3; ref. #2375 in the report).
- A range selection is not announced; only the focused cell is (WCAG 4.1.2, 4.1.3; ref. #2343 in the report).
- Nested column headers are not announced reliably (WCAG 4.1.2; ref. #2373 in the report).
- There is no option to give the grid an accessible description (WCAG 1.3.1; ref. #3044 in the report).
- A cell that shows a formula result is not distinguished from a typed value (WCAG 4.1.2, 4.1.3; ref. #3046 in the report).
- The position of an empty cell is not announced (WCAG 4.1.3; ref. #2441 in the report).
- The audited version’s date picker (Pikaday) was not announced properly. Since 18.0.0 the built-in date cell type uses the browser’s native date input, which the audit did not cover; a custom Pikaday editor keeps the gap (WCAG 4.1.2).
- The loading icon (an SVG) is not hidden from screen readers (WCAG 1.1.1).
- The dialog’s default accessible name is the word “Dialog”; pass
a11y.ariaLabelin the dialog settings to name it after its content (WCAG 4.1.2). - Menu separators are exposed as interactive menu items (WCAG 1.3.1, 4.1.2).
- The checkboxes in the filter menu are labeled “Checked” or “Unchecked” instead of with the value they stand for (WCAG 1.3.1, 4.1.2).
- The summary row and some icons of the Horizon theme fail the contrast ratio (WCAG 1.4.3, 1.4.11).
- With frozen rows, frozen columns, or both, some screen readers read the total number of rows and columns incorrectly.
- When you select a cell in a frozen row or column, NVDA might announce the wrong column header name.
- Screen readers sometimes omit ARIA attributes that change dynamically.
- The
aria-rowcountattribute is set to-1on purpose, because most screen readers ignore or misread it; this keeps VoiceOver accurate. Handsontable revisits this when screen readers handle the attribute consistently.
Result
Your grid now supports keyboard navigation and carries ARIA attributes on its interactive elements. Screen readers announce the focused cell and the menus; the sort order and a range selection are not announced yet (see Known limitations).
API reference
For the list of options, methods, and Handsontable hooks related to accessibility, see the following API reference pages:
Configuration options
Related blog articles
Troubleshooting
Didn’t find what you need? Try this:
Microsoft and Excel are registered trademarks of Microsoft Corporation. Google Sheets is a trademark of Google LLC.