Skip to content

add a11y plugin example#2095

Open
illetid wants to merge 5 commits into
masterfrom
add-a11y-plugin
Open

add a11y plugin example#2095
illetid wants to merge 5 commits into
masterfrom
add-a11y-plugin

Conversation

@illetid

@illetid illetid commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a drop-in Accessibility plugin under plugin-examples/

It attaches one labelled, focusable primitive per pane, so multi-pane / multi-series charts work out of the box. detach() restores the DOM to exactly how it was found.

What it does

  • Semantic layer — each pane gets a focusable overlay (role="application", aria-label, aria-roledescription, aria-describedby); the canvases are hidden from assistive tech (aria-hidden), the layout table is marked presentational, and focusable chart internals (e.g. the attribution link) are taken out of the tab order.
  • Keyboard navigation — ←/→ move between data points (the viewport pages so the whole series is reachable), ↑/↓ switch series, Page Up/Down jump, Home/End first/last, Enter/Space read a summary, H lists the controls.
  • ARIA-live announcements — a per-pane assertive region for navigation, and a single shared polite region for background data updates so multi-pane updates never talk over each other (default announces only the active/last-focused pane; configurable to combine all panes).
  • Visible focus indicator — an optional ring that tracks the active point and stays aligned with the canvas while scrolling/zooming (WCAG 2.4.7).
  • Localization — every announced string is overridable via a messages bundle; dates/numbers follow the chart's localization.locale (and its priceFormatter/timeFormatter if set); a lang attribute is set on the regions so screen readers use the right voice.
  • Lightweight & event-driven — "active-point-only" strategy (never mirrors data points into the DOM); data sync is driven by subscribeDataChanged, so scrolling/zooming does no data work.

Notes & tradeoffs

  • role="application" is used so arrow keys drive data navigation under screen readers; this intentionally suspends the SR virtual cursor while focused on the chart (press Tab to move on — it is not a focus trap). Documented in the README.

@illetid illetid requested a review from SlicedSilver June 9, 2026 14:40
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
ESM 50.81 KB (-0.04% 🔽)
ESM createChart 41.69 KB (0%)
ESM createChartEx 40.44 KB (0%)
ESM createYieldCurveChart 42.62 KB (0%)
ESM createOptionsChart 40.57 KB (0%)
Standalone-ESM 52.3 KB (-0.09% 🔽)
Standalone 52.22 KB (-0.04% 🔽)
Plugin: Text Watermark 1.91 KB (0%)
Plugin: Image Watermark 1.73 KB (0%)
Plugin: Series Markers 4.34 KB (0%)
Series: LineSeries 3.54 KB (0%)
Series: BaselineSeries 4.3 KB (0%)
Series: AreaSeries 4.23 KB (0%)
Series: BarSeries 2.61 KB (0%)
Series: CandlestickSeries 2.91 KB (0%)
Series: HistogramSeries 2.69 KB (0%)
Plugin: UpDownMarkersPrimitive 2.42 KB (0%)

@SlicedSilver SlicedSilver left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks really good and extends beyond what was previously mentioned in the tutorial.

Comment on lines +49 to +72
:::info Ready-made plugin

If you would rather not wire these techniques up by hand, the
[Accessibility plugin](https://github.com/tradingview/lightweight-charts/tree/master/plugin-examples/src/plugins/accessibility)
in the plugin examples packages the keyboard navigation and ARIA layer described
here into a chart-level helper built on pane primitives:

```js
addAccessibilityPlugin(chart, { chartTitle: 'My chart' });
```

Keyboard navigation always covers the whole series (the arrow keys page the
chart as needed). The plugin defaults to `dataScope: 'visible'`, which scopes the
spoken *summaries* to the current viewport – the recommended mode for charts with
large data sets. Use `dataScope: 'all'` when you want those summaries to describe
the full history.

It is also fully translatable: numbers and dates follow the chart's
`localization.locale`, and every announced string can be overridden through a
`messages` bundle (with a `lang` attribute so screen readers use the right
voice). See the plugin's README and its Spanish example.

:::

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest that we also mention the plugin on the first page of the A11y tutorial. Most people would be happy to see that and skip the tutorial by just using a plugin instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a section at the first page

import { generateLineData } from '../../../sample-data';
import { addAccessibilityPlugin } from '../accessibility';

const chart = ((window as unknown as any).chart = createChart('chart', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove = ((window as unknown as any).chart = from the examples

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment on lines +230 to +233
} else {
// Business-day string, e.g. '2019-05-15'.
return time;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultTimeFormatter returns business-day string times verbatim (unlocalised) while timestamp/BusinessDay-object times get toLocaleDateString. Edge case, probably acceptable, but slightly inconsistent localisation.

Comment on lines +984 to +991
case 'PageUp':
event.preventDefault();
this._movePoint(this._options.pageStep);
break;
case 'PageDown':
event.preventDefault();
this._movePoint(-this._options.pageStep);
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageUp moves +pageStep (forward/right in time), PageDown moves backward. This is internally consistent with ARIA slider semantics (PageUp increases the value), so I wouldn't call it a bug — but on a left-to-right time axis some users expect PageDown to advance. The README just says "jump ten points" without direction. A one-line note on direction in the keyboard table would remove the ambiguity.

Comment on lines +976 to +983
case 'ArrowUp':
event.preventDefault();
this._moveSeries(-1);
break;
case 'ArrowDown':
event.preventDefault();
this._moveSeries(1);
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have multiple series (and they don't have all the same timestamps, such as a moving average line not having the first 10 points) then the up / down keys won't select the same points (time) on the different series.

This also means that we don't always scroll the selected point into view when switching series because we assumed that they would have the same index on the time scale but this is incorrect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch, fixed that one and changed moving average to be more realistic in the example

help: ({ multiSeries }): string =>
`Keyboard controls. Left and right arrows move between data points. ${multiSeries ? 'Up and down arrows switch between series. ' : ''}Page Up and Page Down jump ten points. Home and End jump to the first and last points. Enter or Space reads a summary of the series.`,
point: ({ position, total, time, label, values }): string =>
`Point ${position} of ${total}. ${time}, ${label} ${values}.`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When moving to different points (up and down arrows), I think we should mention the price first, then the date, and then the 'point 447 of 500' at the end. Mentioning the point 447... at the start doesn't make sense to me because it is the least important piece of information.

illetid added 4 commits June 10, 2026 11:33
- announce dates in UTC to match the time axis
- use pageStep in the keyboard help text
- treat a viewport scrolled past the data as an empty scope
- announce the newest bar as Latest, not the last visible one
- hide the focus ring when all series are removed
- re-announce identical messages from the standalone polite region
- re-neutralise focusables and canvases the library re-creates
- resolve the host pane dynamically when pane indices shift
- drive init retries via requestUpdate
- read series data lazily instead of caching snapshots
@illetid illetid requested a review from SlicedSilver June 10, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants