Active Foundation

Search documentation

Search all pages, headings and text, in the current package or topic or in all documentation.

Accessibility

The data table, keyboard navigation per archetype, announcements and forced colours, with the evidence and the known limits.

@workspace/active-charts 6.0.0Status unknowncontent differs from the recorded release 6.0.0baseline ce081dc3

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.

ProvisionWhere it holdsEvidence
Accessible name and description on the chart SVGEvery 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 zonesunit, browser (named tables, structure, and no page widening at 390 px)
Values in the Cartesian tableEvery 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 timeunit, browser
Keyboard navigationPer archetype (§ 2), compound glyphs included (W5)unit; browser for Cartesian and compound stepping
TooltipEvery Cartesian item, compound glyphs with every role; the standalone families' own tooltipsunit; browser (a real pointer over each compound family)
Live announcementsPolite 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 selectedunit; 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 highunit; 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 chartunit; browser
Focus indicator2px solid outline, 2px offset, the --ring token (package default when the host sets none) on the SVG and focusable descendantsunit (styles present); contrast not measured
Colour contrast of marksEvery 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 marksA 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 sizeFloors 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 legendsA legend with more entries than fit folds behind a counted "Show N more" button (aria-expanded); every entry stays a named togglebrowser, Chromium and Firefox
Reduced motionEvery 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 claimbrowser, touch emulated in Chromium only; no physical device (§ 8)
Forced coloursA 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 GrayTextbrowser 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.

ArchetypeChartsPrimary keysActivationExit
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 EndSpace → select and Enter → activate when the host turns them on (ADR-042), otherwise both open the context menu; Shift+F10 / ContextMenu → context menu; + = - _ 0 → zoomEscape, blur
CategoricalPie/Donut, Funnel, Nightingale, RadialBar, Radar← → ↑ ↓ Home EndEnter / SpaceEscape, blur
HierarchicalTreemap, Sunburst, OrgChart← → ↑ ↓ (siblings), Enter (drill down), Backspace (drill up)Enter (optionally debounced via confirmDrillDown)Escape, blur
GraphSankeyEnter 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 layerEnter / SpaceEscape, blur
MatrixHeatmap← → 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
TemporalTimeline← → 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 globallyEnter / Space → onActivate(index)Escape, blur
ScalarGauge, 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:

TypeScript
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#

SubfieldDefaultConsumer opt-outConsumer override
labelDerived from chart type + series namesnone: an empty or blank label falls back to the derived name on every family (CH-10, ADR-093); a Sparkline without a name is decorativeaccessibility={{label: 'Custom'}}
descriptionbuildChartDescription() template— (always present)accessibility={{description: 'Custom'}}
dataTableEnabled, caption and x-header derivedaccessibility={{dataTable: false}}accessibility={{dataTable: {caption: 'Custom', xHeader: 'Date'}}}
announcementsAll categories enabledaccessibility={{announcements: false}}accessibility={{announcements: {suppress: ['zoom']}}}
keyboardNavigationEnabled, terse, wrapAround: falseaccessibility={{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.

PairStatus (2026-09-29)
NVDA + Chrome or EdgeNot run. The build environment has no NVDA installation and no human tester.
VoiceOver + SafariNot 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#

This page as Markdown