# Accessibility

> Grid and treegrid semantics with true row and column counts, a full keyboard model, visible focus and WCAG 2.1 AA contrast.

bo-grid follows the ARIA grid pattern and targets **WCAG 2.1 AA**. Rows are virtualized, so only a window of them is in the page at any time; the grid reports the full size and every row's real position, so a screen reader is not misled by the window.

Try it with the keyboard only: <kbd>Tab</kbd> into the grid, move with the arrows, tick a row with <kbd>Space</kbd>, and open the column menu with <kbd>Alt</kbd>+<kbd>↓</kbd> (its **Filter…** item filters the column).

Example: Keyboard only (accessibility.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';
  import { quotes } from './data';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', sub: 'name', width: 170 },
    { type: 'text', key: 'sector', header: 'Sector', width: 130 },
    { type: 'price', key: 'price', header: 'Price' },
    { type: 'percent', key: 'change', header: 'Chg %' },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1 },
  ];
  let rows = $state(quotes());
</script>

<!-- One tab stop: Tab in, then arrows, Space to tick a row, Alt+↓ for the column menu. -->
<Grid {rows} {columns} height={300} ariaLabel="Watchlist" rowSelection columnMenu filterMenu />
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## Give the grid a name

Set `ariaLabel` so the grid has an accessible name ("Watchlist", "Open orders"):

```svelte
<Grid {rows} {columns} height={480} ariaLabel="Open orders" />
```

## Roles and states

- The grid is `role="grid"` (`role="treegrid"` with tree data), with `aria-rowcount` and `aria-colcount` giving the full size, not the rendered window, and `aria-multiselectable` while range selection is on. It holds only rows; the toolbar, pager and menus sit beside it.
- Rows are `role="row"` with `aria-rowindex`; cells are `role="gridcell"` with `aria-colindex` and `aria-selected`. Headers are `role="columnheader"` with `aria-sort`.
- The focused cell is announced through `aria-activedescendant`, so assistive technology follows it as it moves.
- Tree rows carry `aria-level` and `aria-expanded`; the row-number column is made of row headers.
- Sparkline cells have a text label; copies of rows drawn for sticky headers or while loading are `aria-hidden`; the selection's totals bar and the find bar's match count are live regions.

## Keyboard

The grid is a single tab stop with arrow-key navigation inside it. Everything works from the keyboard: editing, selection, copy and paste, the column menu (<kbd>Alt</kbd>+<kbd>↓</kbd>), the row menu (<kbd>Shift</kbd>+<kbd>F10</kbd>), and filtering through the column menu's **Filter…** item. Menus move focus in when they open and give it back when they close, and there are no keyboard traps. The full map is in [Keyboard & hotkeys](https://bonguynvan.github.io/bo-grid/docs/keyboard/).

## Focus, contrast and motion

- Every control that takes focus shows a visible `:focus-visible` ring: headers, menu items, the pager, filter and column panels, tree and detail toggles.
- The built-in themes meet AA contrast for text and for interface parts; the high-contrast presets go well beyond it. See [Theming](https://bonguynvan.github.io/bo-grid/docs/theming/).
- Flashes and other animations respect `prefers-reduced-motion`.
- Every string the grid writes, accessible names included, can be translated; see [Localization](https://bonguynvan.github.io/bo-grid/docs/localization/).

The full audit (keyboard map, roles, measured contrast ratios and conformance notes) is in [ACCESSIBILITY.md](https://github.com/bonguynvan/bo-grid/blob/main/ACCESSIBILITY.md).

## Reference

- `ariaLabel`: `string` (default `undefined`) — Accessible name for the grid (`aria-label` on the `role="grid"` root).
- `labels`: `Partial<GridLabels>` (default `{}`) — Override any string the grid renders itself (menus, filter editor, pager, aria labels). Merged over English defaults — pass only what changes. Interpolated entries are functions so translations can reorder the parts. Resolved per grid.

  ```svelte
  <Grid {rows} {columns} height={480} labels={{ noRows: 'Không có dữ liệu', sortAscending: 'Tăng dần' }} />
  ```

- `hotkeys`: `Hotkeys` (default `undefined`) — Keys for the app, run before the grid's own. A binding is a key (`'b'`, `'Delete'`, `'Space'`; a symbol by the character it types: `'?'`, not `'Shift+/'`), after any of `Shift+`, `Alt+`, `Ctrl+`, `Meta+` and `Mod+` (Ctrl or ⌘, either). Case does not matter. The handler is given the focused row and column (null when there is none) and the event; return false to leave the key to the grid. Keys go to the grid, not to a binding, while a cell is edited or an input, button or menu inside the grid has them, and a key a binding took runs it once when held down. A binding that cannot be read, or binds a key twice, throws (BG1001–BG1004).

  ```svelte
  <Grid {rows} {columns} height={480}
    hotkeys={{ b: ({ row }) => { if (row) buy(row); }, 'Shift+C': cancelAll, Delete: ({ row }) => { if (row) cancel(row); } }} />
  ```

---
Page: https://bonguynvan.github.io/bo-grid/docs/accessibility/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
