Imperative API
Drive scrolling, the scale, zoom to fit, the detail panel and task creation from your own controls.
The chart draws the timeline, and the task list when it is enabled. It draws no toolbar and no scale picker, so build those in your app and call the chart through a ref.
Build the toolbar
ReactGanttChart is wrapped in forwardRef. Attach a useRef<GanttHandle> and the handle holds nine methods once the chart has mounted.
import { useRef, useState } from 'react';
import {
ReactGanttChart,
type GanttHandle,
type GanttScaleKey,
type Task,
} from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';
const SCALES: GanttScaleKey[] = ['day', 'week', 'month', 'quarter', 'year'];
const initialTasks: Task[] = [
{ id: 'design', name: 'Design', parentId: null, sequence: '1',
startDate: '2026-09-01', endDate: '2026-09-08' },
{ id: 'build', name: 'Build', parentId: null, sequence: '2',
startDate: '2026-09-08', endDate: '2026-09-18' },
];
export function GanttToolbar() {
const ganttRef = useRef<GanttHandle>(null);
const [tasks, setTasks] = useState(initialTasks);
const [scale, setScale] = useState<GanttScaleKey>('month');
return (
<>
<div>
<button onClick={() => ganttRef.current?.scrollToToday()}>Today</button>
<button onClick={() => ganttRef.current?.zoomToFit()}>Fit</button>
<button onClick={() => ganttRef.current?.openDetail('build')}>Build details</button>
<select
aria-label="Timeline scale"
value={scale}
onChange={(e) => ganttRef.current?.setScale(e.target.value as GanttScaleKey)}
>
{SCALES.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
<ReactGanttChart
ref={ganttRef}
tasks={tasks}
onTasksChange={setTasks}
defaultScale="month"
onScaleChange={setScale}
showDetail
height={420}
/>
</>
);
}The ref is null until the chart mounts, so optional-chain every call. The buttons keep no state of their own, because each method reads the chart's current state when it runs. Type declarations are in GanttHandle.
Scroll the timeline
scrollToDate and scrollToToday move the horizontal axis only. scrollToTask moves both axes, and it centres the row vertically only when that row is off screen.
ganttRef.current?.scrollToDate('2026-09-01', { smooth: false, align: 'start' });
ganttRef.current?.scrollToTask('build');Every scroll method takes the same options. smooth animates unless you pass the literal false. align centres the target unless you pass the literal 'start', which puts it at the left edge of the timeline area.
Centring measures the viewport with the pinned task list pane subtracted. Horizontally, scrollToTask targets the bar's left edge under align: 'start' and the bar's midpoint otherwise. align has no vertical effect, and both targets clamp at 0.
Every scroll method returns with no error and no console output. An out-of-range date, an unknown task id or a chart that has not mounted is safe to call while data loads.
scrollToTask searches the rendered rows, so a task under a collapsed parent stays out of reach until you expand it.
The first scroll needs no ref. initialScrollTo takes "today" or a date string and scrolls there once after the first render, and later data updates leave the scroll alone.
Keep the picker in sync
setScale writes the scale and keeps the date at the centre of the timeline in place. onScaleChange reports every committed change back. Point a native <select> at both, as the example above does, and the select needs no state of its own.
Wheel zoom, the keyboard shortcuts and zoomToFit() all report through onScaleChange, so the select follows them with no extra wiring. Your app writes the option text, so those labels translate with the rest of your interface. See Locale and date formats.
Fit the whole project
zoomToFit() reads the earliest start and the latest end across the rendered tasks. It tries the five
scales from finest to coarsest and switches to the first one whose span fits the timeline width. When
none fits it uses year, and the earliest date is pinned to the left edge.
The fit is measured against each scale's average pixels per millisecond, which is exact where ticks have a fixed duration. quarter and year use calendar-length cells, so a span within a few percent of the viewport width can land on the neighbouring scale.
zoomToFit() takes no options and its scroll is always instant. It returns with no error when there are no tasks, no mounted chart, or no usable start and end date.
There is no zoomIn or zoomOut. Step through your own array of the five scale keys with setScale, or leave stepping to the wheel and keyboard shortcuts described in The timeline.
Open panels and add tasks
openDetail(taskId) opens the docked side panel on a task, and closeDetail() closes it. Both do nothing while the panel is off, so a button wired to either needs no guard of its own.
openDetail resolves the id against every task the chart holds, collapsed rows included. closeDetail() on a closed panel fires no onDetailChange. With detailTaskId given, the chart is controlled, both methods only report the request, and the panel follows your state. See Detail panel.
addTask() is the only member that concerns data. It builds the same draft as the Add task button, one tick long at today, and passes it to onTaskCreate. It writes nothing, so the row appears when your handler adds the task to tasks.
Without onTaskCreate, or with creation turned off, addTask() does nothing and again needs no guard. See Creating tasks.
Before you integrate
addTask()passes a draft toonTaskCreateand writes nothing. No method edits, moves or deletes a task, so every change still travels throughtasksandonTasksChange.getScrollElement()returns the raw scroll node for your app to drive. There is noscrollToRow, no scroll position getter and no scroll event callback.- No method reports failure. An out-of-range date, an unknown id, an unknown scale key and a detail call on a chart with no panel all look like success from outside.
setScalehas no getter beside it, so the current scale lives in your app's state or nowhere.
Props and defaults: GanttProps.