UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

322 lines (270 loc) 12 kB
--- title: Accessibility description: Give charts useful names, keyboard-equivalent interaction, reduced motion, redundant encodings, and exact-value alternatives. --- Accessibility is part of the chart contract, not a final annotation pass. TanStack Charts provides named SVG and Canvas surfaces plus shared focus primitives; the application still owns the surrounding explanation, controls, and exact-value alternative. ## Name the chart Every DOM host and framework adapter requires `ariaLabel`: ```tsx <Chart definition={definition} ariaLabel="Weekly downloads for three packages" /> ``` Use `ariaDescription` when the reader needs context that is not already visible nearby: ```tsx <Chart definition={definition} ariaLabel="Weekly downloads for three packages" ariaDescription="Values are seven-day totals. Missing weeks are rendered as gaps." /> ``` The SVG renderer emits an image role, a chart roledescription, and a `<desc>` when a description is supplied. The Canvas renderer places the same image role, name, roledescription, description, and tab index on its root while keeping all five canvas descendants `aria-hidden`. Do not put instructions, conclusions, and all underlying data into one enormous accessible name. ## Preserve semantic context outside the surface A chart should usually be accompanied by: - a visible heading; - a short statement of what is being compared; - units and time range; - a source note when relevant; - a summary or table when exact values matter. Use normal HTML for this content. It is easier to navigate, select, translate, and print than text embedded in SVG. ## Keyboard focus and selection Chart-owned focus supports pointer and keyboard navigation over chart points. Leave `keyboard` enabled when a chart is interactive. Focus callbacks expose the same typed `ChartPoint` data regardless of input method. If the application replaces chart-owned focus with a brush, zoom, editor, or rich overlay: - provide semantic buttons, sliders, inputs, or table rows; - keep visible focus styles; - expose current state in text; - support Escape when an interaction can be cancelled; - use live announcements sparingly for meaningful committed changes; - make touch targets at least 44 CSS pixels where practical. The [Interactions and Selections](./interactions-and-selections.md) guide defines the ownership boundary. `interactiveColorLegend` uses native browser buttons with `aria-pressed`, a named group, 44-pixel minimum targets, and stable focus across controlled updates. Static SVG output keeps only a visual fallback, so pair exported charts with text or a table when series visibility must remain actionable. `brushX` exposes its ordered handles as horizontal sliders when `values` is provided. Arrow keys move one authored value, Home and End move to the allowed range edge, and Escape restores the range from the active pointer gesture. Give both handles distinct names and format their semantic values. The DOM overlay works with SVG and Canvas hosts; static SVG and React Native need an application-owned range input for equivalent operation. `continuousCursor` paints an unsnapped pointer guide but does not expose that presentation-only overlay as a keyboard task. Pair its controlled x/y position with named sliders or inputs and visible status text. Escape clears a visible cursor through the behavior; the semantic controls should update the same controlled position. Static SVG and React Native retain accepted guide paint but still need those application-owned controls for operation. `handleX` exposes one named horizontal slider. Left or Down moves to the previous authored value, Right or Up moves to the next, and Home and End move to the candidate endpoints. Its `format` result becomes `aria-valuetext`; the pointer target is 44 pixels high by default. Escape restores the gesture origin while a pointer or touch drag is active. Static SVG and React Native retain the accepted handle paint but need an application-owned semantic input. `zoomX` exposes one named plot surface with visible focus. Plus and minus zoom, the arrow keys pan, Home resets, and Escape cancels an active gesture. Wheel input is captured only while that surface is focused; an unfocused chart must not trap page scrolling. Keep a visible Reset button and current-window text bound to the same controlled window. Static SVG and React Native render the accepted window but need application-owned semantic zoom controls. ## Never rely on color alone Pair semantic color with at least one other signal: - direct labels; - shape or stroke pattern; - ordering; - position; - text in a legend or linked table; - selected outlines and `aria-pressed` state. Check contrast against the actual application background in both light and dark modes. The theme cannot infer whether an arbitrary data color is accessible. ## Motion The default SVG tween and optional `motion()` renderer respect `prefers-reduced-motion` by default. Keep `respectReducedMotion: true` unless an application has a stronger accessible motion policy. Reduced motion snaps entrance, update, and focus-state changes to final geometry without scheduling animation frames. Motion should explain continuity between states. It should not: - delay access to current values; - loop without a pause control; - move the focus target away from keyboard users; - encode the only evidence of a change. See [Dynamic Data and Animation](./dynamic-data-and-animation.md) for the lightweight tween and optional spring renderer boundaries. ## Tooltips are supplemental The built-in tooltip exposes its structured rows through a polite status region. It is not a replacement for a label, axis, legend, or data table. Essential information must remain available without hovering. For complex framework content, use the adapter's tooltip-body composition surface. Its transient body is inert, so a display-only nested chart can remain visible but does not enter the focus or accessibility order. Render controls only while `pinned` is true. The pinned surface has non-modal dialog semantics, Escape and `dismiss()` close it, and focus returns to the chart when dismissal starts inside the body. Controls still need an intentional tab order and accessible names. Use `tooltip.visibility: 'pinned'` when no transient surface should be announced. Focus and pinned mark styling remain available, while the tooltip element and framework body mount only after activation. ## Linked table pattern For analytical and operational charts, a linked table gives readers exact values and a familiar navigation surface. Selection state should be shared semantically, rather than inferred from DOM nodes. Use `keyedSelection` and `whenSelected` for chart activation and selected geometry. Keep the semantic HTML table, status announcement, and clear control in the application, bound to the same controlled selected key. ```tsx group=linked-chart-table env=charts-react file=/src/App.tsx entry import { useMemo, useState } from 'react' import { Chart } from '@tanstack/charts/react' import { createDefinition } from './chart' import { rows } from './data' export default function App() { const [selectedId, setSelectedId] = useState<string | null>(null) const definition = useMemo( () => createDefinition(selectedId, setSelectedId), [selectedId], ) const selected = rows.find((row) => row.id === selectedId) return ( <section> <Chart definition={definition} height={280} ariaLabel="Customer onboarding observations" ariaDescription="Select a point or a table row to inspect the same observation." /> <div> <p role="status" aria-live="polite"> {selected ? `${selected.product}: ${selected.setupMinutes} minutes, satisfaction ${selected.satisfaction}` : 'No observation selected'} </p> <button type="button" disabled={!selected} onClick={() => setSelectedId(null)} > Clear selection </button> </div> <table style={{ width: '100%', borderCollapse: 'collapse' }}> <caption>Customer onboarding observations</caption> <thead> <tr> <th scope="col">Product</th> <th scope="col">Setup minutes</th> <th scope="col">Satisfaction</th> </tr> </thead> <tbody> {rows.map((row) => { const isSelected = row.id === selectedId return ( <tr key={row.id}> <th scope="row"> <button type="button" aria-pressed={isSelected} onClick={() => setSelectedId(row.id)} style={{ minHeight: 44, fontWeight: isSelected ? 700 : undefined, }} > {row.product} {isSelected ? ' — selected' : ''} </button> </th> <td>{row.setupMinutes}</td> <td>{row.satisfaction}</td> </tr> ) })} </tbody> </table> </section> ) } ``` ```ts group=linked-chart-table file=/src/chart.ts import { defineChart, dot } from '@tanstack/charts' import { scaleLinear } from '@tanstack/charts/scales/linear' import { controlledSignal } from '@tanstack/charts/interaction/signal' import { keyedSelection, whenSelected } from '@tanstack/charts/selection' import { rows, type Observation } from './data' export function createDefinition( selectedId: string | null, onSelect: (id: string | null) => void, ) { const selection = keyedSelection<Observation, string, number, number>({ selected: controlledSignal(selectedId, (next) => onSelect(next)), key: (datum) => datum.id, }) return defineChart({ marks: [ dot(rows, { id: 'observations', x: 'setupMinutes', y: 'satisfaction', key: 'id', r: 5, }), whenSelected( dot(rows, { id: 'selected-observation', x: 'setupMinutes', y: 'satisfaction', key: 'id', r: 8, strokeWidth: 3, }), selection, ), ], x: { scale: scaleLinear, axis: { label: 'Setup time (minutes)' } }, y: { scale: scaleLinear, grid: true, axis: { label: 'Satisfaction' } }, selection, }) } ``` ```ts group=linked-chart-table file=/src/data.ts collapsed export interface Observation { id: string product: string setupMinutes: number satisfaction: number } export const rows: readonly Observation[] = [ { id: 'atlas', product: 'Atlas', setupMinutes: 18, satisfaction: 9 }, { id: 'beacon', product: 'Beacon', setupMinutes: 31, satisfaction: 7 }, { id: 'comet', product: 'Comet', setupMinutes: 24, satisfaction: 8 }, { id: 'delta', product: 'Delta', setupMinutes: 42, satisfaction: 6 }, { id: 'ember', product: 'Ember', setupMinutes: 15, satisfaction: 10 }, ] ``` [Open the full linked chart-and-table catalog case](https://tanstack.com/charts/catalog/82-chart-table-selection/). ## Testing checklist Test the finished application, not only the SVG or Canvas element: 1. Navigate the page and chart with a keyboard. 2. Confirm visible focus and a logical navigation order. 3. Operate selection, pinning, zoom, and editing without a pointer. 4. Verify focus and selection persist correctly after data updates. 5. Enable reduced motion. 6. Test light, dark, forced-colors, and increased text size. 7. Inspect the accessible name and description. 8. Confirm exact values are available outside hover-only UI. 9. Confirm status and errors are not encoded only by color. 10. Use a screen reader on the complete task flow. Automated checks can catch missing names and invalid roles, but they cannot decide whether the representation communicates the right thing.