Keyboard editing
A chart can be edited without a pointer. With keyboard.barFocus the bars take the keyboard focus
(one tab stop for all of them), and with keyboard.edit the arrow keys edit the bar that has it:
Left and Right move the task one snap step earlier and later, Shift with them moves its end, and
Alt and Shift its start. Each press is a whole change, with the permissions, the propagation to
successors and children and the before-hooks of the same drag, and onTasksChange hears it with the
reason move, resize-start or resize-end.
The chart also adds a hidden live region (role="status") that says the result, for a screen
reader: the task, its new dates, its working days and how many other tasks the change moved. Each bar
is named with the same facts. The list under the chart is what onTasksChange reported.
pnpm add @ganttlane/dom # vanillapnpm add @ganttlane/react react react-dom # Reactnpm install @ganttlane/dom # vanillanpm install @ganttlane/react react react-dom # Reactyarn add @ganttlane/dom # vanillayarn add @ganttlane/react react react-dom # Reactbun add @ganttlane/dom # vanillabun add @ganttlane/react react react-dom # Reactimport type { Link, Task } from '@ganttlane/core';
// 2026-03-02 is a Monday.export const tasks: Task[] = [ { id: 'brief', name: 'Brief', start: '2026-03-02', end: '2026-03-04', progress: 100 }, { id: 'design', name: 'Design', start: '2026-03-05', end: '2026-03-11', progress: 80 }, { id: 'build', name: 'Build', start: '2026-03-12', end: '2026-03-20', progress: 30 }, // Locked: the arrow keys leave it to the chart, which scrolls. { id: 'review', name: 'Review', start: '2026-03-12', end: '2026-03-18', locked: true }, { id: 'qa', name: 'QA', start: '2026-03-23', end: '2026-03-25' }, { id: 'launch', name: 'Launch', type: 'milestone', start: '2026-03-26' },];
export const links: Link[] = [ { source: 'brief', target: 'design' }, { source: 'design', target: 'build' }, { source: 'build', target: 'qa' }, { source: 'qa', target: 'launch' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { viewMode: 'day', scroll: { initial: 'first-task' }, calendar: { weekends: [0, 6] }, // The bars take the focus, the arrows edit the one that has it, and a click selects. keyboard: { barFocus: true, edit: true, announce: true }, selection: { onClick: true }, layout: { rowHeight: 36, barHeight: 24 },} satisfies Partial<GanttOptions>;import '@ganttlane/dom/styles.css';
import { type ChangeSet, createGantt } from '@ganttlane/dom';
import { links, tasks } from './data';import { options } from './options';
/** What the list shows of a change set: the reason and the tasks it moved. */const line = (changeSet: ChangeSet): string => `${changeSet.reason}: ${changeSet.changes.map((change) => String(change.id)).join(', ')}`;
export function mount(container: HTMLElement): () => void { const root = document.createElement('div'); const chart = document.createElement('div'); chart.style.height = '300px'; const heading = document.createElement('p'); heading.textContent = 'onTasksChange received:'; const list = document.createElement('ol'); root.append(chart, heading, list); container.append(root);
const gantt = createGantt(chart, { tasks, links, ...options, // Every arrow key press that changed something, like a drag, arrives here once. onTasksChange: (changeSet) => { const item = document.createElement('li'); item.textContent = line(changeSet); list.prepend(item); }, });
return () => { gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';
import { type ChangeSet, Gantt } from '@ganttlane/react';import { useState } from 'react';
import { links, tasks } from './data';import { options } from './options';
/** What the list shows of a change set: the reason and the tasks it moved. */const line = (changeSet: ChangeSet): string => `${changeSet.reason}: ${changeSet.changes.map((change) => String(change.id)).join(', ')}`;
export default function Example() { const [received, setReceived] = useState<string[]>([]); const { viewMode, ...rest } = options;
return ( <> <Gantt {...rest} defaultViewMode={viewMode} // `tasks` and `links` are controlled props: these are where the chart starts. defaultTasks={tasks} defaultLinks={links} style={{ height: 300 }} // Every arrow key press that changed something, like a drag, arrives here once. onTasksChange={(changeSet) => { setReceived((before) => [line(changeSet), ...before]); }} /> <p>onTasksChange received:</p> <ol> {received.map((text, index) => ( <li key={received.length - index}>{text}</li> ))} </ol> </> );}Files
data.ts: six tasks and four links; Review is locked.options.ts: the keyboard, the snap step, the weekend and the layout.vanilla.tsandreact.tsx: the handler that lists the changes.
Options
| Option | Value | What it does |
|---|---|---|
keyboard.barFocus |
true |
The bars take the keyboard focus and are named with their dates |
keyboard.edit |
true |
Left and Right edit the bar that has the focus |
keyboard.announce |
true |
Selection changes are said too (editing always says its result) |
selection.onClick |
true |
Enter, Space and a click select the bar, and it says it is pressed |
calendar.weekends |
[0, 6] |
Saturday and Sunday are skipped by a move, as they are by a drag |
scheduling.snap |
'1d' (the view mode’s) |
One press is one day |
onTasksChange |
a function | Every press arrives here, with changeSet.reason |
What to try
- Press Tab until a bar has a ring. Press Right: the bar moves a day, and so do the tasks that follow it. Press Left on a Monday: the bar goes to the Friday before, skipping the weekend.
- Hold Shift and press Right: only the end moves. Hold Alt and Shift and press Left: only the start moves. A task never gets shorter than one step.
- Focus Review and press Right: it is locked, so the key is left to the chart and the chart scrolls.
- Up, Down, Home and End walk the bars, and Enter selects.
- Turn on a screen reader and move Design a day later: it says
Design, Mar 6, 2026 – Mar 12, 2026, 80% done · 5 working days; 3 other tasks changed(Build, QA and Launch follow it).
Notes
- One press, one change. There is no edit mode: a press is committed (or refused) at once, so a
save-on-change application receives one
onTasksChangeper press. A pointer drag is still one. nudge. The same step is a method:gantt.nudge(id, 'move' | 'resize-start' | 'resize-end', -1 | 1), for a button of your own.- A task that cannot be edited that way (locked,
editable: false, oreditingoff for that kind) does not take the key, and the chart scrolls as it would withoutkeyboard.edit.