Skip to content

Accessibility and browser support

This page says plainly what accessibility support the chart has, what is missing, and how to serve users who need more. It then lists the browsers, runtimes and tools the library supports, and how that support is tested.

  • The scroller is a named, focusable region. It has role="region", tabindex="0" and an aria-label taken from strings.chartLabel (default 'Gantt chart'). Translate it with the strings option. Once the region has focus, the browser’s own keyboard scrolling works on it (the arrow keys, Page Up, Page Down, Home and End).
  • A visible focus ring. The focus indicator is drawn with :focus-visible, and its colour and width are the tokens --gantt-focus-ring-color and --gantt-focus-ring-width (theming).
  • Tooltips are described to assistive technology. A tooltip is role="tooltip", and the bar it describes gets aria-describedby pointing at it, for as long as it is shown.
  • The link menu is a menu. It is role="menu" with role="menuitem" buttons. Opening it moves the focus to its first item, Tab moves between the items, and Escape closes it and gives the focus back to where it was.
  • Motion respects the user. Under prefers-reduced-motion: reduce every transition is off.
  • Colour comes from tokens, in a light and a dark theme. With theme: 'auto' the chart follows prefers-color-scheme. Every colour is a --gantt-* custom property, so a high-contrast theme of your own is a matter of overriding tokens (see Theming).
  • What the chart draws is also data. The core carries what an accessible renderer needs: stable task and link ids, row order and depth, the expanded state, the selection, per-task permissions, the names and dates of every TaskRecord, and issues without UI strings. A renderer of your own can build on them (Custom renderers).

With keyboard.barFocus, which is off by default, bars take the keyboard focus with one tab stop for all of them: the focused bar, else the one that last was if it is still drawn, else the first drawn. So tabbing back in never jumps far away. A bar then has role="button" and a name that describes the task in one line, as its tooltip does: its name, its dates, its progress and its working days (Design, Mar 3, 2026 – Mar 7, 2026, 80% done · 4 working days), in the chart’s locale and with its strings. A task with no name is named by its id. Where selection.onClick makes a click select a bar, it also has aria-pressed. The ring shows from the keyboard only (:focus-visible), and it is an outline, so a selected, invalid or conflicting bar shows its own state beside it.

Key What it does
Up, Down The bar of the row above or below; a row without one is skipped
Home, End The first or the last bar
Page Up, Page Down A page of rows
Enter, Space A click: taskClick, and the selection if selection.onClick is on
Ctrl or Cmd + Enter With selection.multiple, adds the task or takes it out; Cmd + Space belongs to macOS
Left, Right Scroll the chart sideways, as on the scroller (or edit, see below)

The keys do nothing while a gesture runs or a link is armed, and Escape still cancels those. The focused bar stays in the document when it scrolls out of view, so it keeps the focus, and it keeps it when rows are sorted. If it goes away with its row, the focus goes to the tab stop. A window that loses the focus keeps the bar as the place to return to, and focusTask(undefined) called from your code takes the focus from the bar to the chart.

Beside a task list, the bars and the rows share one focused task, and the surface that holds the browser’s focus keeps it.

With keyboard.edit (also off by default, and it needs barFocus) the arrow keys edit the bar that has the focus:

Key What it does
Left, Right Moves the task one snap step earlier or later
Shift + Left, Right Moves the end of the task: shorter or longer
Alt + Shift + Left, Right Moves the start of the task: later or earlier

Ctrl, Cmd, or Alt alone with an arrow belong to the browser and the system (Ctrl and an arrow switches the desktop on a Mac), so the chart leaves them alone. Each press is a whole change: the snap step of that gesture (scheduling.snap, else the view mode’s, else the data step), skipped non-working days, the minimum length of one step, the propagation to successors and children, the before-hooks and the permissions of a drag. onTasksChange hears one change per press, with the reason move, resize-start or resize-end; a save-on-change application receives one per press, as it does per drag. A task that cannot be edited that way (it is locked, editable: false, or editing is off for that kind) does not take the key, and Left and Right scroll the chart as they would without the option. The same step is a method, nudge, for buttons of your own (Methods).

There is no edit mode to enter, and so nothing to cancel with Escape.

With keyboard.undo and history on, Ctrl or Cmd + Z undoes the last action, and Ctrl or Cmd + Shift + Z or Ctrl + Y redoes it (Cmd + Y is the browser’s history, so it is left alone). The keys work while the focus is on a bar or on the chart’s scrollable region and nowhere else: an input, a contenteditable or a control of a slot or a bar label keeps its own undo, and so does the rest of the page. They do nothing during a drag or while a change waits to be applied (controlled data). A key that has nothing to do, with no history or with nothing to undo or redo, is not taken from the browser (no preventDefault). A Latin letter is read from the typed character, so Dvorak and AZERTY follow what is printed; on a layout that types a letter of another script (Cyrillic, Greek) the physical Z and Y keys are used. A symbol or an accented letter on those keys is no combination. Holding the key repeats it, and each repeat undoes one more step, as a text field does. With announce, or edit with barFocus, the result is said in the live region: Undone. Design, Mar 2, 2026 – …, Redone. …, or only Undone for a link.

Screen readers that have a browse mode (NVDA and JAWS on Windows) keep the arrow keys for their own cursor there, and the keys reach the chart only in the mode that passes them on (focus mode). Such a reader switches to it on its own for a grid or a dialog, and may not for a button, so a user may have to switch (NVDA: Insert + Space). This has not been tried with a real reader.

keyboard.announce, and keyboard.edit with barFocus (nothing can speak without a focusable bar), add a hidden live region (div.gantt__live, role="status") to the chart. A keyboard change says the task with its new dates, progress and working days, and how many other tasks the change moved (for example Design, Mar 3, 2026 – Mar 7, 2026, 80% done · 4 working days; 1 other task changed), or that the task was not changed (a hook cancelled it, or it is at its minimum length). announce also says when the selection changes other than by a press on the bar that has the focus and says it is pressed (select(), a task list): Design selected, 3 tasks selected, Selection cleared. The words are keys of strings, taskChanged, notChanged, selection, undone and redone, which are functions of the values and the locale, so that a language with more plural forms can say it right. The keyboard editing example shows both.

These are not available in this version:

  • The chart has no grid roles of its own. The task list is the grid: a role="treegrid" with rows and cells, which the keyboard walks cell by cell when you turn on its cellNavigation. The bars are buttons beside it.
  • Links and the handles of a bar are not focusable. The arrow keys on the bar resize it instead of the handles. There is no keyboard way to change the progress, to draw a link, or to open the tooltip (tooltip.trigger: 'focus' is reported as unsupported).
  • The contrast of the default themes is pinned by a test of the token pairs (WCAG AA 4.5:1 for text, 3:1 for bars, links, the focus ring and the invalid and conflict states). A theme of your own has to be checked by you; the tokens are in Theming.

If your users rely on assistive technology or a keyboard, give them the same plan in a form that works for them, next to the chart, and let them edit it there:

  • The task list, a role="treegrid" that the keyboard walks, kept in sync with the chart. Add a column for what your users need.
  • A table or a list of your own (name, start, end, progress, predecessors), built from the same data you give the chart. For an uncontrolled chart, read it from getTask(id) and the onTasksChange event; for a controlled one the data is already yours.
  • Edit forms. A dialog that edits a task through updateTask(id, patch) gives a keyboard-and-screen-reader path to every change that dragging makes. Connect it to the chart with the click event of a task (onTaskClick), or to a button in your own list. The programmatic updates example and the edit session example show the calls.
  • Links by form. addLink and removeLink do what drawing and removing a link do, without a pointer.

Both the table and the chart then show one source of truth, and any change in either shows in the other.

The chart is the vanilla chart, so its accessibility is the same. What React adds:

  • id, aria-* and data-* props go to the container div, so you can label the whole widget.
  • The content you draw through the slots (the tooltip, the link menu, the bar label) is your React, so its semantics, labels and focus behaviour are yours. A tooltip or menu that holds buttons or inputs is interactive: it takes pointer events, a link menu also moves the focus to its first button, and Escape or a press outside closes it. See React.

Tab to the chart and check that the focus ring shows and that the name is announced; with keyboard.barFocus, edit and announce on, Tab to a bar, move it with the arrow keys and listen to what is said, with the screen readers your users have (VoiceOver, NVDA, JAWS); switch the OS to a high-contrast or dark colour scheme and check each theme you ship; turn on prefers-reduced-motion and check that nothing animates; open the tooltip and the link menu with a keyboard-only session and a screen reader.

The chart supports the last two versions of Chrome, Edge, Firefox and Safari, and iOS Safari 16.4 or newer.

Feature the chart uses Where Needs
Pointer Events, ResizeObserver, SVG the renderer every supported browser
Cascade layers (@layer), :is(), overflow: clip the stylesheet every supported browser
color-mix() the stylesheet Chrome and Edge 111, Firefox 113, Safari and iOS Safari 16.2
A regular expression with a lookbehind the core’s JavaScript iOS Safari 16.4, the oldest iOS Safari that can load the core

The progress colour has a fallback for the one thing in the list that is newest (color-mix()), and every browser above has all four stylesheet features.

The end-to-end suite runs in Chromium, Firefox and WebKit through Playwright, in a production build of the playground: scrolling, pointer events and layout differ between engines, and that is what a chart is made of. Real touch input is tested in Chromium only, and iOS momentum scrolling is not tested automatically. The React bindings are run in a production build, in a development build (StrictMode) and on React 18.3.

  • Left to right only. .gantt sets direction: ltr whatever the page says, and direction: 'rtl' is reported as unsupported.
  • A browser is needed for the chart. createGantt draws into a DOM element: on a server, import types only, or use @ganttlane/core. There is no server-rendered markup of the vanilla chart. @ganttlane/react renders its container on the server and draws the chart after hydration.
  • A closed shadow root. A chart in an open shadow root works if you adopt the stylesheet into the root; a closed one is not supported.
  • Clipping ancestors with automatic height. With layout.height: 'auto', no ancestor between the chart and what scrolls it may clip with overflow: hidden.
Piece Support
@ganttlane/core Node 20 or newer; the same browsers as above. Standard ECMAScript and Intl only. No DOM, no timers, no window or document. It runs in a worker as it is
@ganttlane/dom A browser with a DOM
React @ganttlane/react for React 18.3 and 19 (react and react-dom are peer dependencies, ^18.3.0 || ^19.0.0). The whole unit-test suite runs against both
Module format ESM only, with TypeScript declarations. TypeScript must resolve package exports: moduleResolution of bundler, node16 or nodenext
Bundlers Anything that understands ESM, package exports and CSS imports. The packages mark CSS as a side effect (sideEffects: ["**/*.css"])
TypeScript Typed throughout; TypeScript 6 works. The stylesheet subpath ships a declaration file, so import '@ganttlane/dom/styles.css' type-checks (see the FAQ)

The core reads the system clock only for “now” (replace it with the now option), so it is deterministic in tests, and it starts no timers of its own.

React 18.3 differs from 19 in a few ways the React guide lists: how StrictMode reuses state on the first render, the absence of <Activity> (19.2 only), a warning about useLayoutEffect that a server render with a document (a jsdom test) provokes, and the type of a useRef<GanttHandle>(null).