The timeline

Pick the scale, control the date range, and shade weekends and holidays.

The timeline decides how much time fits on screen. Pick the scale it opens at, fit or pin its date range, and shade the days nobody works.

Choose a starting scale

Five scales are built in, finest first. defaultScale picks the one the chart opens at and defaults to month.

ScaleCell holdsDrag stepPx per dayPadding
day6 hours1 hour28830 hours
week1 day6 hours725 days
month7 days1 day1835 days
quarter1 month7 days45 months
year1 quarter28 days115 months

Each step out is about four times coarser, and a header cell stays 60 to 130px wide. A quarter or year cell follows the calendar. The drag step is the grid a bar snaps to, covered in Editing tasks.

ProjectChart.tsx
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';

const initialTasks: Task[] = [
  { id: 'spec', name: 'Spec', parentId: null, sequence: '1',
    startDate: '2026-06-10', endDate: '2026-06-13' },
];

export function ProjectChart() {
  const [tasks, setTasks] = useState(initialTasks);

  return (
    <ReactGanttChart
      tasks={tasks}
      onTasksChange={setTasks}
      defaultScale="week"
      height={420}
    />
  );
}

defaultScale is read once at mount; later changes are ignored. onScaleChange reports every committed change, from the ref, wheel zoom, the keyboard or zoomToFit(), and never on mount.

The chart draws no scale picker. Wire one to setScale on the ref, reading its value back from onScaleChange. See Imperative API.

Fit the timeline to your data

With no range props the chart fits the timeline to the rolled-up task dates, earliest startDate to latest endDate, padded by five ticks at each end. Cells start there, snapped back to a tick boundary, so month week columns open on a Sunday.

The last cell can finish past the padded end. See Task list and hierarchy.

onRangeChange reports the range as two Dayjs values. end is exclusive, the last cell's start plus one tick, so an inclusive query counts the last tick twice.

It fires on the first render with cells, then whenever those dates change, which a scale change almost always causes. The handler is read through a ref, so replacing it does not re-fire it; a late one sees only the next change.

A vertical line marks today, with a dot in the header above it. No prop adds another marker or moves, labels or hides this one; CSS on .gantt-today-marker and .gantt-today-dot restyles it. The date is read when the timeline recomputes, so it never moves at midnight and vanishes when today leaves the range.

Pin or extend the range

visibleStart and visibleEnd take ISO strings and pin the timeline instead of fitting it. A pinned start snaps back to a tick boundary; a pinned end is used as given.

Pin both and task dates stop affecting the layout. A task outside the window keeps its row, its bar clamped to the nearest edge at the minimum width.

Pin one end and the other auto-fits with its padding. With no tasks and one end pinned, the chart renders an empty element and throws nothing.

infiniteScroll grows the range as people scroll, off by default. Coming within half a viewport of either end adds about a viewport of ticks there, up to 2000 per side.

The front is checked first, so a chart mounted at the far left extends straight away. Cells added in front shift the scroll, keeping the view on the same dates. A pinned end never grows, and a scale change drops what has accumulated.

Let people zoom

zoomOnWheel is off by default, leaving Ctrl or Cmd with the wheel to page zoom. Turn it on and that gesture steps one scale, wheel down to zoom out.

The date under the cursor keeps its x position. A cursor over the task list anchors on the timeline's left edge, and one past the last cell cancels the zoom.

Ctrl or Cmd with ArrowUp steps one scale finer and ArrowDown one coarser, needing no prop. It anchors on the timeline's centre and announces the new scale. See Keyboard and screen readers.

Both stop at the finest and coarsest scale. Plain wheel scrolls vertically and Shift with the wheel horizontally, either way. zoomToFit() on the ref fits every rendered bar in one call, as Imperative API shows.

Shade weekends and holidays

showNonWorkingDays is on by default, shading non-working days behind the bars and writing holiday names in the tick row. workingWeekdays sets the weekly rule in UTC day numbers, 0 being Sunday, and defaults to Monday to Friday. holidays lists the exceptions, matched against each tick's UTC date.

const WORKING_WEEKDAYS = [1, 2, 3, 4, 5, 6];
const HOLIDAYS = [
  '2026-06-06',
  { date: '2026-09-24', endDate: '2026-09-26', label: 'Chuseok', color: '#7c3aed' },
];

<ReactGanttChart
  tasks={tasks}
  onTasksChange={setTasks}
  workingWeekdays={WORKING_WEEKDAYS}
  holidays={HOLIDAYS}
/>

Both arrays are read by identity, so a fresh literal each render rebuilds the calendar. Keep them in a module constant or a useMemo.

A bare YYYY-MM-DD string is a day off with no name. The object form adds an inclusive endDate, a label and a color. Where two entries cover a day, the earlier one wins.

A name appears in the tick row only where its band is at least 44px wide, so the shutdown above is named and the one-day offsite is not. Hover a narrower band in the header to read it.

Where two names open in the same cell, only the first is shown. Names are hidden with opacity rather than display, so a screen reader announces them anyway.

A color is tinted at the weekend's weight, and a holiday band stays separate from the weekend beside it. One on a day already off keeps its name and colour.

CSS on .gantt-holiday-cell restyles every name. color is the only per-holiday style, and nothing adds a class or an icon to one holiday.

Shading follows the tick, not the scale name. day, week and month shade, quarter and year do not, because a month tick cannot be half non-working. Each day in a multi-day cell is checked on its own, and adjacent non-working days merge into one band.

Before you integrate

  • Zoom is five fixed steps, with no pixels-per-day prop, no fractional zoom and no custom scale.
  • defaultScale is not persisted. A reload starts there again, so store the scale yourself and pass it back.
  • showNonWorkingDays shades days and nothing else. false hides the shading and the names, and a dropped bar lands on a Saturday unless workingCalendar is on. See Working calendar.
  • firstDayOfWeek regroups the week scale's top header only. It moves no shading, and month week columns still open on a Sunday.
  • holidays entries are read leniently. A range expands to at most 366 days, a backwards endDate collapses to its start day, and an unparseable date drops it.

See GanttProps for the props and Scale and theme types for the Holiday shape.