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.

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.

KeysWhat happens
ArrowUp / ArrowDownOne row up or down, keeping the column. Clamped, never wrapping.
ArrowRight / ArrowLeftExpand or collapse from an expandable row's first cell. Otherwise one cell across, clamped.
Home / EndFirst or last cell of the row. With Ctrl or Cmd, of the first or last row.
Enter / SpaceToggle 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.

KeysWhat happensCapability
Alt + horizontalMove the task, both dates.canMove
Shift + horizontalMove the end date.canResize
Alt+Shift + horizontalMove the start date.canResize
+ / = and - / _Progress up or down 5 points.canChangeProgress
Delete / BackspaceDelete the task and its subtree.canMove
Alt + verticalMove the row among its siblings.canReorder
Ctrl/Cmd + horizontalIndent under the previous sibling, or outdent one level.canReorder
Ctrl/Cmd + verticalStep 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.

AfterAnnouncement
a date editDesign phase, Mar 3, 2025 to Mar 15, 2025
a progress stepDesign phase, 45% complete
a deleteDesign phase deleted
a row moveDesign phase moved to 2 of 5 under Phase 1, or at the top level
a blocked editDesign phase cannot be changed, progress cannot be changed, cannot be deleted
a scale stepweek 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().
  • Delete removes 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 in onTasksChange to undo it.
  • A bar scrolled out of the viewport horizontally is not rendered, so the roving tab stop can have no element. Tab then finds nothing in the treegrid, and Home or End restores 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, an F2 edit mode, multi-select and Escape in 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-colors or prefers-contrast block. prefers-reduced-motion cuts transitions and animations to 0.01ms and scroll behaviour to auto.

None of this is verified against a real screen reader. Test with the readers your users have. Props and defaults: GanttProps.