---
id: "packages/active-charts/accessibility"
title: "Accessibility"
description: "The data table, keyboard navigation per archetype, announcements and forced colours, with the evidence and the known limits."
url: "/packages/active-charts/accessibility"
locale: "en"
revision: "c78c0d54de57b4e4"
---

# Accessibility

> [!NOTE]
> **The package's own guide**
>
> This page is the package's `docs/accessibility/ACCESSIBILITY.md`, included
> unchanged from the package as it is in Foundation. Links to the package's
> internal design documents keep their text.

This document is the **support statement** of `@workspace/active-charts`: what each chart family provides for people who do not use sight or a pointer, how to configure it, the evidence behind each provision, and the known limits.

**Scope of the claim (W6, ADR-035).** The package aims at the WCAG 2.2 level AA success criteria that a chart component can meet on its own (text alternatives, info and relationships, keyboard, focus visible, name, role and value, status messages). It makes **no conformance or certification claim**: a page is conformant only as a whole, and several criteria (headings, colour contrast of the surrounding interface, the host's words) belong to the host. Each provision below states its evidence level, and nothing is claimed beyond it. The earlier "WCAG 2.1 Level AAA" statement and "verified" assistive-technology matrix were withdrawn on 2026-09-25 because no recorded evidence supported them.

**What every supported family guarantees** (ADR-036): all plotted meaning is available through at least one non-visual path. Cartesian charts give a data table, keyboard stepping with announcements and a tooltip for every item, compound glyphs included; every standalone family gives a data table with its own structure next to its keyboard model; scalar charts give a table and a rich accessible name; the sparkline is decorative (§ 7).

If something in your application doesn't match what's described here, open an issue — accessibility regressions are treated as bugs, not enhancements.

## 1. What is provided, and at which evidence level

Evidence levels: **unit** is a jsdom test in the package suite; **browser** is the real-browser suite (`pnpm --filter @workspace/active-charts test:browser`, Chromium for the full set and Firefox for a subset), without assistive technology; **AT** is a recorded human run with a screen reader (§ 8). **No AT run is recorded yet.** The ET-3 qualification protocol (ADR-120, `BROWSER-AT-CHECKLIST.md` § ET-3 qualification) and its UI Lab fixtures are in place; until ET-3C records human rows and GATE 3 passes, nothing below is qualified on a screen reader, a phone, a real Contrast theme or by a native reader. The dated rows behind every level, per browser, are in `EVIDENCE.md`.

| Provision | Where it holds | Evidence |
| --- | --- | --- |
| Accessible name and description on the chart SVG | Every chart family except the sparkline, which is decorative (§ 7) | unit |
| Data table (`accessibility.dataTable`) | Every family except the sparkline. Cartesian: one row per displayed datum. Standalone: a table with the family's structure (§ 3). Scalar: value, range, target and zones | unit, browser (named tables, structure, and no page widening at 390 px) |
| Values in the Cartesian table | Every series type: one column per value role for candlestick, OHLC, box plot, range area and range bar; a waterfall's drawn amount, its kind (change, subtotal, total) in words and its running balance (ADR-097); grouped rows with interval and count. Instants are listed with their time | unit, browser |
| Keyboard navigation | Per archetype (§ 2), compound glyphs included (W5) | unit; browser for Cartesian and compound stepping |
| Tooltip | Every Cartesian item, compound glyphs with every role; the standalone families' own tooltips | unit; browser (a real pointer over each compound family) |
| Live announcements | Polite live region for navigation, zoom, legend and context-menu events (the period selector conveys its state with `aria-pressed`, not an announcement). Compound points announce every role, a waterfall bar its kind, its drawn amount and the balance (a total its balance), legend changes the name the legend shows. The most recent announcement speaks, so a keyboard reader hears zoom, legend, pin and menu events while stepping; every way of opening the context menu (right click, Shift+F10, the context-menu key, a long press) is announced (ADR-052) | unit; browser |
| Item selection and activation (ADR-042, ADR-102) | `<Chart>`, the thirteen Cartesian wrappers and, since ADR-102, the nine standalone families with selection (pie, funnel, radial bar, nightingale, radar, heatmap, treemap, sunburst, sankey), when the host turns them on. Space selects the focused item and Enter activates it (on a sankey Shift+Enter, because Enter moves between nodes and links); arrow keys move focus only, so focus is never selection. A selected item carries a filled caret beside or inside it (never an outline, since ADR-070), its name and its table cells read "…, selected", the focused item is announced with its selected state, and every change is announced. A hidden, zoomed-out, regrouped or drilled-out item is not presented as selected | unit; browser (pointer, keyboard, touch, table, announcements, forced colours; Chromium and Firefox) |
| Context menu (ADR-087) | One Base UI menu in every family except the Sparkline, the same as the Active Grid's. Opened by right click, Shift+F10, the ContextMenu key, a long press (500 ms) or the optional menu button; a keyboard opening focuses the first item; arrow keys, Home, End and typeahead move; ArrowRight (ArrowLeft in rtl) opens a submenu; Escape closes one level; Tab closes the menu; focus returns to the opener. A disabled item stays focusable and exposes its reason as `aria-description`. Every family announces the opening. A host item receives the frozen context with the element, legend entry or chart the menu was opened on. Under a coarse pointer every item is at least 44 px high | unit; browser (Chromium, Firefox; WebKit in the clean-consumer proof) |
| Linked views (ADR-042) | In a `SyncGroup`, the keyboard focus of one chart marks the same x on the others (a decorative, `aria-hidden` mark); focus, announcements and the table stay with the focused chart | unit; browser |
| Focus indicator | 2px solid outline, 2px offset, the `--ring` token (package default when the host sets none) on the SVG and focusable descendants | unit (styles present); contrast not measured |
| Colour contrast of marks | Every default categorical colour reaches 3:1 against the page and the card, in light and dark (W7, ADR-038); the first six stay distinct under simulated dichromacy, and lines beyond six carry a dash pattern (`../QUALITY-BUDGETS.md`) | browser (asserted) |
| Text on marks | A label drawn on a mark takes the on-fill colour that reaches 4.5:1 against it, or moves off the mark or is left out (its value stays in the tooltip and table) | browser (asserted, light and dark) |
| Text size | Floors per role: ticks and in-chart labels 10 px, legend 12 px, hierarchy text 10 px at the initial fit (W8) | browser (asserted at 320, 390, 768, 1440 px) |
| Dense legends | A legend with more entries than fit folds behind a counted "Show N more" button (`aria-expanded`); every entry stays a named toggle | browser, Chromium and Firefox |
| Reduced motion | Every motion class collapses to zero (entrance, update, exit, grouped slot reflow, tooltip fade and travel, locator departure, attention, colour-key marker, replay, discrete zoom; the motion resolver and the stylesheet), whatever the host configures; no option overrides it (ADR-104); the default entrance is 400 ms with at most 200 ms of stagger otherwise. Dumbbell, Lollipop, Bullet and error bars included (QR-3) | browser, under Chromium's reduced-motion emulation (asserted: nothing moves) |
| Touch (emulated) | A swipe over an unzoomed chart scrolls the page; a pinch zooms a zoom-enabled chart and not the page; a zoomed chart pans under one finger; a long press opens the context menu, which stays open; the navigator brush follows a sideways drag. Not a support claim | browser, touch **emulated** in Chromium only; no physical device (§ 8) |
| Forced colours | A `forced-colors: active` stylesheet (series strokes and patterns for the first six lines, matching legend keys, focus ring, tooltip; every chart text in `CanvasText` on a `Canvas` halo, so a label stays readable on a mark that keeps its own colour, whatever the host mode, ADR-108; the unavailable state's sentence too, since CH-69). Asserted on the line, bar, candlestick and pie families and, since ADR-052, on Dumbbell, Lollipop, Bullet, error bars, range, box plot, scatter, area and the standalone families: every text in a system colour, every series with a visible mark; since ADR-100 on the scalars: needle, bar, track markers and targets in `CanvasText` (target and value told apart by form), scale ticks in `GrayText` | browser under Chromium's forced-colors **emulation** only (asserted and in screenshots) |

## 2. Keyboard Navigation

Navigation is archetype-shaped: Cartesian uses a scanning model; Categorical/Hierarchical/Graph/Matrix/Temporal point `aria-activedescendant` on the SVG at the focused item (items carry `role="img"`; there are no listbox or option roles). In all cases the SVG is the single tab-stop.

| Archetype | Charts | Primary keys | Activation | Exit |
| --- | --- | --- | --- | --- |
| Cartesian | `<Chart>` (any mix of series), Line, Area, Bar, Scatter, Histogram, Candlestick, OHLC, RangeArea, RangeBar, BoxPlot, Dumbbell, Lollipop, Waterfall (a compound glyph is one item) | `←` `→` `↑` `↓` `Home` `End` | `Space` → select and `Enter` → activate when the host turns them on (ADR-042), otherwise both open the context menu; `Shift+F10` / `ContextMenu` → context menu; `+` `=` `-` `_` `0` → zoom | `Escape`, blur |
| Categorical | Pie/Donut, Funnel, Nightingale, RadialBar, Radar | `←` `→` `↑` `↓` `Home` `End` | `Enter` / `Space` | `Escape`, blur |
| Hierarchical | Treemap, Sunburst, OrgChart | `←` `→` `↑` `↓` (siblings), `Enter` (drill down), `Backspace` (drill up) | `Enter` (optionally debounced via `confirmDrillDown`) | `Escape`, blur |
| Graph | Sankey | `Enter` switches between the nodes and the links (CH-68; `Tab` leaves the chart), `←` `→` `↑` `↓` step through nodes / links, `Home` / `End` jump to first / last item in the current layer | `Enter` / `Space` | `Escape`, blur |
| Matrix | Heatmap | `←` `→` step column within row, `↑` `↓` step row within column, `Home` / `End` jump to first / last column of current row (row-local) | `Enter` / `Space` → `onActivate(row, col)` | `Escape`, blur |
| Temporal | Timeline | `←` `→` step through events in data order (ascending time with `timeAxis`, ADR-103), `↑` `↓` jump between tracks (multi-track only; cursor lands on the nearest event in the target track), `Home` / `End` first / last event globally | `Enter` / `Space` → `onActivate(index)` | `Escape`, blur |
| Scalar | Gauge, LinearGauge, Bullet | — (no stepping: one value; the name and the data table carry value, range, target and zones) | — | — |

`aria-keyshortcuts` on the SVG advertises the currently available key set to assistive technology, so users can discover the shortcuts without reading the docs.

## 3. Screen Reader Support

Three parallel channels broadcast information:

- **Accessible name** — `aria-label` on the SVG (or `<title>` where legacy readers insist). Generated by `generateAriaLabel()` on `<Chart>`, from the chart type and name on the standalone families and from value and range on the scalar charts; overridable via `accessibility.label` (an empty or blank label is no name, CH-10). A scalar's name reads its range ("0 in the range −50 to 50" where `min` is not 0; a value beyond an end "150, above the range 0 to 100", without a share, T7), states its target whenever there is one (0 included; under the host's name; "(above the scale)" beyond an end; a share of the target only where it means something), never a tick, a missing categorical value reads "(no value)", and a hierarchy node's DOM id comes from its host `id` or its sibling position, never its label; a sankey node's DOM id encodes its host `id` (any character, injectively, CH-32), so every `aria-activedescendant` resolves while the host id itself is unchanged.
- **Pre-navigation description** — `<desc>` element wired via `aria-describedby`. Default template: `"Interactive chart. {type}. {n} items. Press Tab to focus, arrow keys to navigate."` (the nav hint is omitted when `keyboardNavigation: false`). Overridable via `accessibility.description`.
- **`aria-live` announcements** — a polite live region under the SVG echoes hover/focus, zoom, legend-toggle and context-menu events. Suppressible per-category via `accessibility.announcements` on `<Chart>` and its wrappers.
- **Copy outcome (ADR-116)** — a copy from the menu is never silent: its outcome is announced in the chart's live region after "menu closed", so it is the last thing said (`charts.contextMenu.copied`, or `charts.contextMenu.copyFailed` when the browser rejects the write). A copy the browser cannot run here (no clipboard API, an insecure context) is a disabled item with its reason (`charts.contextMenu.copyUnavailable`, read through `aria-description`), from a pointer and from Shift+F10 alike. A chart that is gone when the write settles announces nothing.
- **Hierarchy drill and return (ADR-121, CH-67)** — on a treemap or a sunburst with `drilldown`, a drill into a group announces "Drilled into {name}" and a return announces "Returned to {name}" (`charts.accessibility.drilledInto`, `charts.accessibility.returnedTo`), once per drill or return that actually happens, from the keyboard (`Enter`, `Backspace`) and from the pointer (a group, the breadcrumb, the sunburst's centre) alike. The name is the label of the level landed on; the top level without a label reads as the breadcrumb's root word (`charts.drilldown.root`), another group without one as `charts.accessibility.unnamedGroup`. Nothing is said on the first render, for `Backspace` at the top level, for `Enter` on a leaf or for a re-render or a data update. The keyboard focus ends with the old level; the next arrow lands on the first item of the new one and replaces the sentence. Not yet qualified with a person and a screen reader (ET-3C, QJ-15 and QJ-16).

### Announcement modes

On navigable archetypes (Cartesian + Categorical + Hierarchical + Graph + Matrix + Temporal) announcements come in two verbosities:

- `terse` (default): `"{seriesName}: {xValue}, {yValue}"` — minimal, fast. Matrix reads `"{colLabel} × {rowLabel}: {value}"` (tooltip-parity); sparse cells fall back to `"(no value)"`, and the keyboard reaches every position of the matrix, a cell with data or not, through one focus node (ADR-117). Temporal reads `"{label}, {date}"`.
- `verbose`: adds position context — `"{seriesName}: {xValue}, {yValue}, point {index+1} of {total}"`. Matrix appends `", cell N of M"` using a 1D flat count (`row * cols + col + 1` of `rows * cols`). Temporal appends `", {category}"` when the focused event has a category (no suffix on single-track timelines).

Dates in announcements are formatted through `@workspace/active-format` using the chart's resolved locale, so announcements match the surrounding UI language.

### Data tables

`accessibility.dataTable` (default on) renders a visually hidden table next to the chart.

- **Cartesian** families list one row per **displayed** datum (W4, ADR-034). A reduced or grouped table is described (`aria-describedby`) as showing N of M rows, or as summarising intervals; a grouped row reads its x cell as the interval and has a column with its number of source rows. A compound series has one column per value role, headed "{series}: {role}"; a waterfall has its step and its running total. A cell whose host row carries no value reads "(no value)", as the tooltip says it, never a blank (ADR-111). The CSV export, not the table, carries every raw row.
- **Standalone** families keep their structure (ADR-036): pie, donut and nightingale list category, value and share; funnel and radial bar category and value; radar one row per variable with a column per series; the heatmap is a matrix of row and column labels while that is no larger than the list of its present cells, else that list (row, column, value, in the displayed order) with a summary that counts the cells listed out of the plane and is the table's description; an absent cell is "(no value)" in the matrix and no row in the list, never 0 (ADR-117); treemap and sunburst list every node's path from the shown root with its summed value, and the tooltip and the announcement read that same value, "(no value)" for a node without one, never 0 (ADR-111); sankey lists source, target and flow; the org chart lists name, parent and title; the timeline lists event, the host's date text and track; gauge, linear gauge and bullet list value, range, target and zones. The caption is the chart's accessible name. Every data table, Cartesian and standalone, sits in a clipping container, so it cannot widen a narrow page (PH1-03, closed by ADR-038).

**Cell values follow the same presentation as the visible chart.** Numbers and dates run through the central formatters of `@workspace/active-i18n` with the chart's own `locale` input, so a reader hears the decimal separator, grouping and date order that the axis shows. This also holds for the per-series **custom** formatters that a few modes install (error bars): they receive the presentation as a third argument, so assistive output never disagrees with the visible axis under a non-default notation. A percentage stack reads its shares in percentage points through the same semantic formatting (W9); a hidden member of a stack has no share and an empty cell. A chart whose input breaks its contract is one `role="img"` element named by the host's `charts.unavailable` text (W12); a supported shape beyond a structural limit (a treemap, sunburst or org chart deeper than 139 levels, ADR-115, ADR-119) is the same element named by `charts.unavailableWorkload`, so a reader and a support desk can tell "this data is wrong" from "this data is too large to show here". Picture and text alternative are present together or absent together: an unavailable chart has no data table.

## 4. Configuration

Every chart accepts an `accessibility` prop of shape:

```ts
interface AccessibilityOptions {
  label?: string; // overrides aria-label
  description?: string; // overrides <desc>
  dataTable?: boolean | AccessibilityDataTableOptions; // default: true
  announcements?: boolean | AccessibilityAnnouncementOptions; // default: true
  keyboardNavigation?: boolean | AccessibilityKeyboardNavigationOptions; // default: true
}

interface AccessibilityDataTableOptions {
  enabled?: boolean;
  caption?: string;
  xHeader?: string;
}

interface AccessibilityKeyboardNavigationOptions {
  enabled?: boolean;
  announcementMode?: 'terse' | 'verbose';
  wrapAround?: boolean;
  confirmDrillDown?: boolean; // hierarchical only
}

interface AccessibilityAnnouncementOptions {
  enabled?: boolean;
  suppress?: ChartAnnouncementCategory[]; // 'zoom' | 'legend' | 'tooltip' | 'contextMenu' | 'period' | 'selection'
}
```

Example:

```tsx
<LineChart
  data={data}
  xKey="date"
  yKey="value"
  xAxis={{temporalSemantic: 'calendarDate'}}
  height={300}
  accessibility={{
    label: 'Monthly NAV, last 12 months',
    description: 'Line chart. 12 data points. Use arrow keys to inspect.',
    keyboardNavigation: {announcementMode: 'verbose'},
    announcements: {suppress: ['zoom']},
  }}
/>
```

### Propagation via `<ChartDefaults>`

Place shared a11y defaults on a provider:

```tsx
<ChartDefaults accessibility={{keyboardNavigation: {announcementMode: 'verbose'}}}>
  <LineChart {...} />
  <BarChart {...} />
</ChartDefaults>
```

Per-chart props always win. Merging is per-subfield (`label`, `description`, `dataTable`, `announcements`, `keyboardNavigation`); a prop that supplies only one subfield inherits the rest from the context.

## 5. Defaults vs. overrides — quick matrix

| Subfield | Default | Consumer opt-out | Consumer override |
| --- | --- | --- | --- |
| `label` | Derived from chart type + series names | none: an empty or blank label falls back to the derived name on every family (CH-10, ADR-093); a `Sparkline` without a name is decorative | `accessibility={{label: 'Custom'}}` |
| `description` | `buildChartDescription()` template | — (always present) | `accessibility={{description: 'Custom'}}` |
| `dataTable` | Enabled, caption and x-header derived | `accessibility={{dataTable: false}}` | `accessibility={{dataTable: {caption: 'Custom', xHeader: 'Date'}}}` |
| `announcements` | All categories enabled | `accessibility={{announcements: false}}` | `accessibility={{announcements: {suppress: ['zoom']}}}` |
| `keyboardNavigation` | Enabled, `terse`, `wrapAround: false` | `accessibility={{keyboardNavigation: false}}` | `accessibility={{keyboardNavigation: {announcementMode: 'verbose'}}}` |

### Uncertainty and zones (ADR-096)

- **Error bars** are stated, never only drawn: the tooltip, the data table and the keyboard announcement read `value (low–high)` (or the named lower or upper bound), in the series' notation; a scatter's x bounds add a table column and follow the x in the announcement. A whisker is no pointer or keyboard stop of its own.
- **Named zones** are read in the tooltip, in one table column per series with named zones and in the announcement; an unnamed zone is presentation and adds no text. No colour name is ever announced.
- **Soft bounds** are presentation only: nothing is announced.

### Hierarchy and flow (ADR-099)

- **Label policy.** A treemap's or sunburst's `label.levels` and `label.secondary` change only what is drawn. Every node keeps its accessible name, its row in the data table, its tooltip and its announcement whatever the policy hides.
- **Treemap nested groups.** Every internal node now has a drawn element behind its keyboard focus (before ADR-099 a nested group's `aria-activedescendant` pointed at nothing).
- **One measure for sight and text (ADR-112).** A treemap's or sunburst's areas, arcs and shares are drawn from the same resolved measure the table, the tooltip, the keyboard reading, the announcement, Copy value and the CSV state, so a sighted reader and a screen-reader user get the same proportions. A parent value that contradicts its children is no longer drawn one way and read another: the chart shows its translated unavailable state (W12).
- **Heatmap coordinates (ADR-113).** Each distinct coordinate (by item key: `1` and `"1"` are two) is its own row or column for the keyboard, the table, selection and the announcement; two typed coordinates no longer collapse into one position with one of them unreachable. Two rows on one cell show the unavailable state.
- **Sankey keyboard.** ArrowLeft and ArrowRight step the nodes column by column as drawn, top to bottom; ArrowUp and ArrowDown stay in the drawn column, also under `nodeAlignment` and `nodeOrder`. The data table keeps one row per link in the order of `links`. Colour (`linkColor`) is never the only carrier: the tooltip, the table and the announcement name both ends.
- **Org chart collapse.** With `collapsible`, a card with reports is a `role="button"` with `aria-expanded`; its name adds "collapsed, 3 hidden" or "expanded". Enter and Space toggle it; Shift+F10 and the ContextMenu key open its menu (Collapse, Expand, Expand all below). The keyboard walks the visible cards only. A toggle is announced through the chart's live region; when the host collapses an ancestor of the focused card, the focus moves to that ancestor and is announced. The data table lists the whole organisation, collapsed or not; an export's caption says what is collapsed.
- **Org card fields.** Every `cardFields` value is in the tooltip and has its own table column, whatever the card can draw.
- **Org keyboard menu (ADR-107).** Shift+F10 and the ContextMenu key open the focused card's menu on every card, collapsible or not; Enter opens it on a card without collapse.
- **Org zoom presets (ADR-107).** The zoom level is a menu button (`aria-haspopup="menu"`, `aria-expanded`, `aria-controls`) whose name starts with the level it shows. Opening it, by click, Enter, Space or an arrow key, moves focus into the menu: to the checked preset, or to the first or last item. The arrows, Home and End move between the presets (`menuitemradio`, `aria-checked` at the current level) and Fit; Escape, Tab and a choice close the menu and return focus to the level. The toolbar keeps full strength while it holds focus, so a focused control is never dimmed; its hover changes stop under reduced motion and with `animation={false}` (ADR-107).
- **No room (RS3, ADR-107).** Below its room a family draws no marks and says so in a caption and in its description; the name, the data table and a scalar's readout stay, so nothing it holds is lost to a reader. Without marks it has no keyboard item model either (CH-63): it is one image, not a tab stop, no arrow key steps or speaks and `aria-activedescendant` never appears; the table is the route to its values. When the room returns, the keyboard model returns with no focused position. Heatmap, radar, timeline and radial bar behave so since ET-3B; the sankey already did.
- **Sankey under forced colours.** Nodes and flows keep their authored hues, as every categorical identity fill of the standalone families does (pie, treemap, sunburst); a flow's colour (source, target or a gradient between them) is node identity, never a scale, so nothing it says becomes false. Recede is off, every flow is drawn at full opacity, labels take `CanvasText` and the keyboard flow the `Highlight` edge; source, target and value stay in the name, the tooltip and the table.

### Quantitative scales (ADR-098)

- A colour measure is stated, never only painted: the treemap and sunburst tooltip, the data table (a column headed by the measure's name) and the keyboard announcement say "Funding ratio: 131" (with the class of a classed scale, "131, Covered"), or "(no value)"; the description names the measure and its scale. No colour name is announced.
- A bubble's size is stated: the tooltip row is labelled with `sizeName`, the data table has a size column per bubble series, a missing size reads "(no value)". The size key is one image named in words ("Bubble size: Assets, from 0 to 1000.", with `≤`/`≥` sentences where values reach beyond), never a control or a focus stop.
- The heatmap's rows and columns are named as the gutters write them (formatter or date notation) in the tooltip, the table and the announcement, in the one displayed order the keyboard follows.
- Forced colours (Chromium and Firefox emulation): the gradient bar collapses with the cells (CH-27 extended to the ramp), measure fills collapse to `Canvas` with a `CanvasText` edge, and the size key keeps its circles in `CanvasText` (size needs no colour). No system-colour gradient is invented.

### Waterfall kinds (ADR-097)

- A subtotal and a total are named, never only painted: the tooltip, the data table (a kind column, `charts.accessibility.stepKind`) and the keyboard announcement say "Subtotal" or "Total" with the amount the bar draws, and the balance. The word comes from the row's declared kind, never from its position, so zooming never renames a summary.
- A subtotal and a total share one paint (and one forced colour, `GrayText`); a subtotal floats and a total stands on zero, and the words carry the difference. Every summary row is a pointer and keyboard stop, whatever its own value field holds; a change without a value is none and its table amount is empty.

### Family singles (ADR-103)

- **Histogram.** The bin is named as its interval ("30 to under 40", the last bin "60 to 70 inclusive") and its amount, in every mode. With `areaProportional` the tooltip adds the drawn rate in a row of its own ("Per 10"); the label, the announcement and the table keep the count or sum, so the rate never reads as the count. The table lists the raw rows.
- **Funnel.** A context-aware formatter changes the value text where the formatter already reached: the label, the table's value cell and the announcement, one text everywhere. The share column, the tooltip (its own `valueFormatter`) and the CSV are unchanged.
- **Timeline.** `date` stays the words a reader hears; machine `time` decides the position and, with `timeAxis`, the reading order (above). No new focus stop.
- **Candlestick.** The body border is drawing only; the OHLC values, the table and the announcement are unchanged. Forced colours keep a rising (hollow) body's border at `bodyStrokeWidth: 0`.
- **Bar.** `inside-end` and `inside-base` move a label within its bar; a hidden label's value stays in the tooltip, the table and the announcement.
- **Sparkline.** `domain` changes geometry only: the host's accessible name and the CSV keep every value, nothing is clamped in text.

### Standalone activation and selection (ADR-102)

- **Off by default.** Without `selection` and `onItemActivate` no standalone family changes a key, a click or a word: Enter and Space keep opening the menu (pie, funnel, radial bar, nightingale, radar, a treemap or sunburst leaf), drilling (a parent) or moving between nodes and links (sankey); the heatmap keeps them unused.
- **The keys.** With selection on, Space toggles the current item; with a handler, Enter activates it (Shift+Enter on a sankey). Shift+F10 and the ContextMenu key open the menu on the current item, with or without selection and activation (on every standalone family with a keyboard item, the timeline included). `aria-keyshortcuts` names only the keys that apply; no tab stop is added.
- **The radar's vertex.** With selection or activation on, the keyboard is on one vertex: the arrows step the variables and PageUp / PageDown the visible series at the current one. The announcement and the item's name name that one vertex ("Plan B, Risk: 7"); without the props the keyboard stays on the variable and names every series (T8-X4).
- **What a reader hears.** No `aria-selected` (every item is `role="img"`): a selected drawn item's name ends in ", selected", the current item is announced with that state, and every change speaks once ("Selected: …", "Deselected: …") in the chart's one live region, where the most recent source speaks. The data table reads ", selected" on the cells of a selected drawn item (a sankey node has no row of its own: the table lists flows).
- **A request is not a fact (ADR-105).** On a chart whose host holds the selection (`selectedItems`) or the collapsed cards (`collapsedIds`), a reader's change is a request. Nothing of it is drawn, listed, read as selected or announced until the host passes it back; the announcement then comes on the commit that applies it, once. A request the host ignores stays silent, and the next one is computed from the host's state again. The host decides when; the chart decides what is said.
- **Not drawn is not selected.** A slice the legend hides, a hidden radar series, a missing radar value and a leaf outside the drilled subtree are not named, listed or counted as selected; they stay in the selection and return when drawn.
- **Leaves and parents.** On a treemap and a sunburst only a leaf takes Space and Enter as selection and activation; a parent keeps drilling (with `confirmDrillDown` as before).
- **Forced colours** (Chromium and Firefox emulation): every family's caret takes `Highlight` over `Canvas`; no family palette was added.
- **Touch.** A tap selects and activates like a click; `multiple` needs no modifier; a long press opens the menu and selects nothing.

### Host composition (ADR-105)

- **A host legend.** A legend a host renders with `useChartLegend()` acts through the chart's own legend route: every hide, show, isolate and Show all is announced in the chart's live region with the entry's label, the last visible target cannot be hidden, and a hidden target leaves the plot, the table and the keyboard order, exactly as with the built-in legend. The host owns its legend's DOM: roles (buttons with `aria-pressed`, or a checkbox list), names, disabled states (from `entry.available`), focus order and keys. The package injects no ARIA into host markup and moves no focus.
- **Only applied truth is announced.** A controlled selection or collapse is announced when the host applies it, never at the request (§ Standalone activation and selection).
- **The input changes no meaning.** `pointer`, `touch` and `keyboard` name how a change started; a selection, an activation, a legend action and a menu action mean the same, are drawn the same and are announced the same whichever input made them.

### Compound grouping and orientation (ADR-101)

- **Keyboard order.** Grouped range bars, boxes and dumbbells are stepped category by category, then series by series in series order, then (dumbbell) low before high; the stable `(datumIndex, seriesIndex)` sort decides, never the drawing order. A horizontal chart steps in the same order. One stop per range bar, box and range-area row, two per dumbbell row; grouping adds none.
- **Identity.** A grouped glyph names its series in the tooltip, the announcement and the data table; several dumbbell series name the series before the end ("Plan: 2024"); one dumbbell series keeps its end names.
- **Orientation is geometry only.** A horizontal box plot reads min, Q1, median, Q3, max in the same order and words as an upright one; a reversed axis mirrors the drawing and never a role.
- **Hidden series.** A hidden series has no stop, no pointer target and no domain share; the others close its gap.
- **Forced colours** (Chromium and Firefox emulation): grouped range bars keep the per-series system colours of CH-28 and their position; boxes, dumbbell markers and range-area edges stay visible; both keys of a paired dumbbell legend entry stay visible. No category palette was added.

## 6. Reduced motion, forced colors, and print

- `@media (prefers-reduced-motion: reduce)` — the motion resolver sets every class to zero (ADR-104), so no scripted motion starts and every interaction duration the stylesheet reads is `0ms`; a defense-in-depth rule in the package stylesheet, which the package entry loads, drops every interaction transition as well. Asserted in Chromium and Firefox per class: no running animation and no in-between frame for an entrance, an update, a hide or show with its reflow, a replay, a tooltip, attention and the colour-key marker; everything still arrives.
- **Motion never carries meaning (ADR-104).** The current item, the tooltip's content, its accessible text, `aria-activedescendant`, the keyboard cue, the selection and every live-region announcement change in the commit of the event (for a controlled selection, the commit in which the host applies the request), never after a transition; a gauge replay leaves the accessible name and the table at the resting value; a grouped reflow keeps every series and item identity. The tooltip's fade and travel and the attention timing are presentation only, and `animation: false` turns them off without losing any of it.
- `@media (forced-colors: active)` — series strokes switch to system colours with a dash pattern for the first six line series (the legend keys follow), every keyboard cue and the selection mark to `Highlight` (on a `Canvas` underlay), the current mark takes a `Highlight` edge (the only data-mark edge W-FV-1 allows; ADR-070), the tooltip background to `Canvas`. Evidence: assertions and screenshots under Chromium's forced-colors emulation; no run in Windows High Contrast mode is recorded.
- `@media print` — the chart prints as drawn, on its own mode's background, with its colours exact (ADR-108): every on-mark label keeps the 4.5:1 it was given against its surface, in light and dark. Interaction overlays (tooltip, crosshair, navigator, transient marks) are not printed and nothing prints dimmed. A host that wants a light print prints in the light mode. Opt out via a higher-specificity stylesheet.

## 7. Known limitations

- **Forced colours and bars** (closed by CH-26, ADR-093): bars, histogram bins and waterfall steps take system colours under forced colours (the first five bar series `CanvasText`, `LinkText`, `Highlight`, `ButtonText`, `GrayText`; a waterfall increase `Canvas` with an edge, a decrease `CanvasText`, a subtotal and a total `GrayText`), and a bar's legend key takes its bar's colour. A bar series beyond the fifth repeats `CanvasText`. Since CH-28 a rising candle is hollow (`Canvas` with a `CanvasText` edge) and a falling one filled (`CanvasText`), with solid `CanvasText` wicks, and range bars take the bar series colours, their legend keys too.
- **Heatmap classes under forced colours** (CH-27, Walt 2026-10-05): the authored class colours collapse with the cells and their legend keys; the class stays a name in the legend, the tooltip, the table and the announcement, and the marker under the current cell's class shows it without colour. A missing cell keeps no edge. A cell does not show its class in text.
- **Scalar archetype (Gauge, LinearGauge, Bullet)**: one focusable value, no arrow-key stepping (W6: stepping a single value has no item to reach). The rich `aria-label` (`computeScalarAriaLabel`) and the data table carry the value, range, target and zones. Ticks are drawing only: never in the name or the table (T7). A gauge's `replay` (T7) adds no tab stop and no key: it is a hidden pointer and touch extra at the pivot, and its keyboard route is the chart menu (Shift+F10 › Replay animation) where the host keeps the item; it changes no reading, announces nothing, and under reduced motion the item is disabled with its reason.
- **Sparkline**: decorative by default (W6) and then hidden from assistive technology (`aria-hidden`); not focusable, no table and no announcements. When it carries meaning, the host names it with `accessibility={{label}}`, which makes it one image with that name (`role="img"`, T0), or states the meaning in the surrounding text; the package does not invent a stepping model for it.
- **Temporal data-order** — without `timeAxis` the keyboard cursor steps through `data` in the exact order the consumer supplies; a timeline with chronologically out-of-order events steps in that order, so sort `data` by date when chronological traversal is required. With `timeAxis` (T9, ADR-103) the keyboard, the data table and the position in the announcement follow ascending machine `time`, equal times in the host's order, and the drawn left-to-right order is the reading order; the CSV keeps the host's order.
- **Temporal pan via keyboard** — mouse pan (drag) is not available via keyboard. Timelines wider than the viewport scroll via the focus-follows pattern (the focus indicator indirectly indicates which event is active), but the visual pan-offset is unchanged by keyboard navigation.
- **Heatmap headers with the same text** (ADR-113) — two distinct coordinates can read the same header (the number `1` and the text `"1"` both read "1"), so a matrix reader navigating by header text hears two equal headers. The package invents no disambiguating words (that would put text in the host's data); the remedy is the axis `formatter`, which receives the raw value and can tell the two apart. A candidate for ET-3 qualification.
- **Sparse heatmap table form** (ADR-117) — the data table switches between the matrix and the list of present cells by a structural rule; the summary before the list says which shape the reader is in. Both forms, the switch and a "find the value at a row and column" task are ET-3 qualification items (no human run yet).
- **Deep hierarchies** (ADR-115) — no depth throws. Treemap, sunburst and the org chart deeper than their provisional boundary of 139 levels (ADR-115, ADR-119) show the workload-unavailable state instead of a drawing and a table; keyboard navigation that deep is not a usable journey (no claim). ET-3 qualifies the workload-unavailable wording; ET-4 measures the boundary.
- **Matrix sort-stability** — the keyboard cursor tracks `(row, col)` coordinates into `uniqueX` / `uniqueY`, which are derived from `data` insertion-order. Re-sorting `data` with the same unique labels moves the cursor to whichever datum now occupies that coord — same semantics as Categorical's index-based stability. Consumers who need focus to follow a specific datum across sorts should persist the focused label themselves and re-seek after the sort.
- **Cartesian pre-navigation `<desc>`** — updates on data length changes; screen readers that cached the description on first focus may need a refocus to re-read.
- **Cartesian focus model** — Cartesian charts use a physical focus + hover-pipe model: the SVG is the single tab-stop and arrow keys move a virtual cursor whose position is reflected by the standard hover highlight. Unlike the archetype hooks, Cartesian therefore intentionally emits no `aria-activedescendant`; per-point ids are not generated. The hook still returns `containerProps` for shape-parity but its `aria-activedescendant` is always undefined.
- **Several dumbbell series** (corrected by ADR-101): the earlier reason for "single-series only" (that the Cartesian sort `(datumIndex, seriesIndex)` would interleave low/high pairs across series) was wrong: the sort is stable and the plug-in emits each series' low before its high, so the order is category → series → low → high. The real gap was identity, closed by ADR-101: with several dumbbell series every end is read with its series first ("Plan: 2024") in the tooltip, the announcement and the table, and the legend keys each series once.
- **DumbbellChart voronoi emits 2N flatpoints** — for `N` rows the voronoi pipeline emits `2N` flatpoints (low first, then high, per row). Keyboard navigation therefore steps low → high → next-low → next-high. This is the only Cartesian series-type that emits multiple flatpoints per row; consumers using a custom `tooltip.renderer` should expect each endpoint to surface as its own hover event.
- **Selection on the standalone families** (ADR-102) — the org chart, the timeline, the gauges, the linear gauge, the bullet and the sparkline have no item selection; their Enter and Space keep their own meaning (the org chart's collapse among them). On a treemap and a sunburst only leaves are items.
- **The Sparkline has no context menu** (ADR-087): it has no focusable item and lives in a cell or a sentence, where a menu would compete with the host's own. Its handle exports.
- **Areas under forced colours** are drawn as outlines: the fill takes `Canvas`, the edge a system colour.
- **Selection is not linked** — charts in a `SyncGroup` share positions and ranges, not selections; a host that wants two charts to show the same selection passes both the same `selectedItems`.
- **ARIA 1.3 `aria-description`** — emitted for future-proofing but not yet consistently surfaced by NVDA/JAWS. The `aria-describedby` + `<desc>` pair is the primary channel.

## 8. Browser & assistive-technology evidence

**Supported pairs (W6):** NVDA with Chrome or Edge on Windows, and VoiceOver with Safari on macOS. The manual procedure is `BROWSER-AT-CHECKLIST.md`. Human evidence is recorded there with date and versions, never inferred from ARIA or from tests.

| Pair | Status (2026-09-29) |
| --- | --- |
| NVDA + Chrome or Edge | **Not run.** The build environment has no NVDA installation and no human tester. |
| VoiceOver + Safari | **Not run.** VoiceOver needs macOS; the build environment is Windows. |

Physical phones and Windows High Contrast are not run either. The qualification that will change this table is ET-3 (ADR-120): H1 NVDA with Chrome in full, H2 NVDA with Edge as a mandatory smoke, H3 VoiceOver with Safari in full, HC1 real Contrast themes, P1 a physical iPhone with iOS Safari, N1 and N2 native Arabic and Hebrew readers. Its automated prerequisites (ET-3B, Chromium, Firefox and a WebKit screen) are screening, not qualification. What does exist is automated browser evidence without assistive technology (Chromium 153 for the full suite, Firefox 155 for a subset, and a one-off WebKit 26.6 run of a subset, environment E3, which is not Safari), recorded per row in `EVIDENCE.md`: roles and accessible names, data-table contents for every family, keyboard stepping and announcements for the Cartesian and compound families, a real pointer over each compound glyph, no page widening by the standalone tables, and the zoomed-waterfall and instant cases; since ADR-042 also item selection by pointer and keyboard, the selected state in the table and in announcements, the selection mark under forced colours (a mark beside the item in `Highlight`, never an outline, W-FV-1, ADR-070), and linked marks in a `SyncGroup`; since ADR-052 also the context menu by keyboard, pointer and long press, reduced motion and forced colours on the remaining wrappers, the horizontal percentage table, and emulated touch. Until the human runs are recorded, this statement is limited to that evidence.

## References

- [WAI-ARIA 1.2 Authoring Practices — Managing focus with `aria-activedescendant`](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant) (the technique of the archetype hooks)
- `@workspace/active-i18n` formatters — used for locale-aware announcement values
