Keyboard and screen readers
What a keyboard user can do in the chart, what a screen reader announces, and what still needs a pointer.
Someone who cannot use a mouse can move, resize, reorder and delete tasks from the keyboard. A live region announces each edit, and a few things still need a pointer.
Navigate with a keyboard
The task list and the timeline form one role="treegrid", and each row claims its bars through
aria-owns. One handler on it reads every key press, handles the fourteen keys below, and lets the
rest bubble out to your app. Each key it handles is preventDefaulted, so Alt+ArrowLeft never
navigates the browser back.
| Keys | What happens |
|---|---|
ArrowUp / ArrowDown | One row up or down, keeping the column. Clamped, never wrapping. |
ArrowRight / ArrowLeft | Expand or collapse from an expandable row's first cell. Otherwise one cell across, clamped. |
Home / End | First or last cell of the row. With Ctrl or Cmd, of the first or last row. |
Enter / Space | Toggle an expandable row. Otherwise announce the row, and open the detail panel when it is on. |
Edit with a keyboard
A modifier claims a horizontal arrow before it reads as navigation, so Shift+ArrowRight resizes
and Ctrl+ArrowRight indents. There is no range selection.
| Keys | What happens | Capability |
|---|---|---|
Alt + horizontal | Move the task, both dates. | canMove |
Shift + horizontal | Move the end date. | canResize |
Alt+Shift + horizontal | Move the start date. | canResize |
+ / = and - / _ | Progress up or down 5 points. | canChangeProgress |
Delete / Backspace | Delete the task and its subtree. | canMove |
Alt + vertical | Move the row among its siblings. | canReorder |
Ctrl/Cmd + horizontal | Indent under the previous sibling, or outdent one level. | canReorder |
Ctrl/Cmd + vertical | Step the scale finer or coarser. | none |
One step is the current scale's drag step, read at the key press. It is added in calendar units, so a press across a daylight saving boundary keeps the time of day. Zooming changes what every editing key does; the new scale is announced, the new step is not, and the timeline lists the step per scale.
There is no delete capability; a task that may move may be deleted. A summary row ignores resizes
and progress steps whatever its flags say, as does a task with no progress field. The flags
resolve as they do for a drag; see interaction configuration.
Navigation, collapsing and the scale steps need no capability and work on a read-only chart. The
reorder keys need allowReorder, read-only or not, and indent and outdent also need hierarchy.
Ctrl/Cmd+ArrowLeft outdents one level per press, landing the row after the parent it left. A
row three deep takes three presses, where a drag reaches the root in one drop.
The chart renders no scale picker, so without a pointer those two keys are the only built-in way
to change the scale. A native <select> wired to setScale brings the platform's keyboard
behaviour with it; the component is in imperative API.
What screen readers hear
A bar is labelled with its name, summary on a summary row, its dates, then its progress when it
has one. The dates use the current scale's tooltip formatter, so locale and a
formats[scale].tooltip override change what is spoken. See locale and date formats.
Keyboard edits announce through one role="status" aria-live="polite" region, the chart's first
child. The wording is fixed English; only the dates in it follow locale and formats.
| After | Announcement |
|---|---|
| a date edit | Design phase, Mar 3, 2025 to Mar 15, 2025 |
| a progress step | Design phase, 45% complete |
| a delete | Design phase deleted |
| a row move | Design phase moved to 2 of 5 under Phase 1, or at the top level |
| a blocked edit | Design phase cannot be changed, progress cannot be changed, cannot be deleted |
| a scale step | week scale |
A row move that is not applied announces nothing, and neither does expanding or collapsing a row.
That silence covers either end
of the sibling list, an indent with no previous sibling, an outdent from the root, a chart without
allowReorder, and an onTaskMove returning false.
A pointer drag announces on a second role="status" line on the bar, hidden for a move or a
resize and visible as a progress drag's percentage readout. The date the tick row writes during a
drag is aria-hidden. showTooltip={false} removes that line with the hover card, so a chart with
tooltips off announces nothing while a pointer edits, though keyboard announcements are unaffected.
The hover card opens after a 450ms dwell, is dismissible with Escape, is not hoverable, and
never opens on touch. Its dates and progress are already in the bar's label.
Tab order and focus
Nothing is focused on mount, and the first Tab lands on the treegrid's first cell. An empty
tasks array leaves no cell to land on.
The treegrid holds one roving tab stop. With the splitter, the Add task button and the open
detail panel's name, Start, End, Progress and close controls, the chart adds up to
eight tab stops to the page. A field the task's flags leave uneditable renders as text and takes no
tab stop, and your own renderDetail body replaces the panel's five.
Clicking a cell syncs the focus state to it, and a keyboard move scrolls a row back into view
before focusing it. Arrowing onto a bar does not select it, and no aria-selected is written. See
events.
The detail panel is role="complementary", outside the treegrid, with no focus trap. Closing it
returns focus to the element that opened it, but only while focus is in the panel, on <body> or
nowhere.
Outside the treegrid, the splitter takes the horizontal arrows and Enter. While a dependency
arrow is selected, Delete, Backspace and Escape reach the document, skipped inside an input.
Escape also closes the open panel from anywhere, except inside one of its own inputs, where it
reverts that field.
Known gaps
- Dependency arrows have no keyboard path. The link handles are
tabIndex={-1}, and selecting an arrow, so deleting one, needs a click. - Drawing a new task needs a pointer, but the Add task button under the task
list produces the same draft from an ordinary tab stop. With the pane hidden, bind your own
shortcut to
addTask(). Deleteremoves the focused row's whole subtree from any cell in the row, with no confirmation and no snapshot to restore from. Keep the previous array inonTasksChangeto undo it.- A bar scrolled out of the viewport horizontally is not rendered, so the roving tab stop can have
no element.
Tabthen finds nothing in the treegrid, andHomeorEndrestores the stop while focus is still inside. With the task list hidden every cell is a bar, so this can empty the whole treegrid. PageUp,PageDown, type-ahead, anF2edit mode, multi-select andEscapein the treegrid are not handled. No prop remaps a key or turns keyboard editing off on its own.- High contrast has no styles of its own, and the stylesheet carries no
forced-colorsorprefers-contrastblock.prefers-reduced-motioncuts transitions and animations to 0.01ms and scroll behaviour toauto.
None of this is verified against a real screen reader. Test with the readers your users have. Props and defaults: GanttProps.