Skip to content

Scrolling and today

Where a chart is looking is part of its options. scroll.initial says where it opens, scroll.align and scroll.behavior say how a scroll that code asks for lands and moves, and three methods do the asking: scrollToToday(), scrollToDate(date) and scrollToTask(id). The today line and its marker in the header are display.today, and the line under the chart is written from the scroll event: every scroll, whether the user or a method made it, reports where the chart is and which dates are in view (getVisibleRange()).

Terminal window
pnpm add @ganttlane/dom # vanilla
pnpm add @ganttlane/react react react-dom # React
View on GitHub
data.ts
import type { Link, Task } from '@ganttlane/core';
// Fourteen tasks from February to July 2026: wide enough to scroll in both directions, and tall
// enough that scrolling to a task moves the rows too. "Today" is pinned to 15 April (see
// `options.ts`), which is in the middle of the plan.
export const tasks: Task[] = [
{ id: 'discovery', name: 'Discovery', start: '2026-02-02', end: '2026-02-13', progress: 100 },
{ id: 'design', name: 'Design', start: '2026-02-16', end: '2026-03-06', progress: 100 },
{ id: 'prototype', name: 'Prototype', start: '2026-03-09', end: '2026-03-20', progress: 100 },
{ id: 'backend', name: 'Backend', start: '2026-03-23', end: '2026-04-24', progress: 60 },
{ id: 'frontend', name: 'Frontend', start: '2026-03-23', end: '2026-05-08', progress: 40 },
{ id: 'docs', name: 'Docs', start: '2026-04-13', end: '2026-05-15', progress: 15 },
{ id: 'review', name: 'Review', start: '2026-05-11', end: '2026-05-22' },
{ id: 'qa', name: 'QA', start: '2026-05-25', end: '2026-06-05' },
{ id: 'beta', name: 'Beta', start: '2026-06-08', end: '2026-06-19' },
{ id: 'release', name: 'Release', type: 'milestone', start: '2026-06-22' },
{ id: 'launch', name: 'Launch', start: '2026-06-23', end: '2026-06-26' },
{ id: 'retro', name: 'Retrospective', start: '2026-06-29', end: '2026-07-03' },
{ id: 'marketing', name: 'Marketing', start: '2026-04-27', end: '2026-06-19' },
{ id: 'hiring', name: 'Hiring', start: '2026-02-16', end: '2026-07-03', progress: 35 },
];
export const links: Link[] = [
{ source: 'discovery', target: 'design' },
{ source: 'design', target: 'prototype' },
{ source: 'prototype', target: 'backend' },
{ source: 'prototype', target: 'frontend' },
{ source: 'backend', target: 'review' },
{ source: 'frontend', target: 'review' },
{ source: 'docs', target: 'review' },
{ source: 'review', target: 'qa' },
{ source: 'qa', target: 'beta' },
{ source: 'beta', target: 'release' },
{ source: 'release', target: 'launch' },
{ source: 'launch', target: 'retro' },
];

Files

  • data.ts: fourteen tasks from February to July, with links.
  • options.ts: scroll, the pinned now, display.today, and what the selects offer.
  • helpers.ts: describe, which writes the status line.
  • vanilla.ts: scrollTo*, setOptions({ scroll }), on('scroll'), and a new chart when scroll.initial changes.
  • react.tsx: the same with props, the ref handle and a key.

Options

Option Value What it does
scroll.initial 'today' Where the chart opens; the select tries each value (read when the chart is created)
scroll.align 'center' Where the target of a scroll lands in the viewport: 'start', 'center', 'end' or 0 to 1
scroll.behavior 'smooth' How scrollTo* moves: 'smooth', 'instant' or 'auto' (the browser’s own)
display.today true The today line and marker; the checkbox switches it
now () => '2026-04-15' The source of “today” (the system clock by default); pinned so that the demo never changes
viewMode 'week' The scale of the timeline (React: defaultViewMode, as nothing here changes it)
onScroll a function (vanilla: on('scroll', …)) { x, y, date } on every scroll: the offsets in px and the date at the left edge

What to try

The chart opens on today (15 April, the red line), centred. Pick another Opens at and the chart is made again: first-task puts the earliest start in the middle, start and end put the left or the right end of the timeline at the edge of the chart (whatever the alignment), and the last choice is a date, 1 June. Then press the buttons and watch the line under the chart: Review scrolls to its bar and to its row, because scrollToTask moves both axes. Set Align to end and press Review again: its bar ends at the right edge and its row is the last one in view. Set Behavior to instant and the scroll jumps instead of gliding. Uncheck Today marker and the line and the header marker go away.

Notes

  • scroll.initial is read once, when the chart is created, which is why the select makes a new chart (destroy() and createGantt() in the vanilla code, a new key in React). align, behavior and display.today are ordinary options: setOptions({ scroll: { align } }) or a prop changes them on the chart that is there.
  • The values. 'today' is the default. 'first-task' is the earliest start (today when there are no tasks). 'start' and 'end' are the ends of the timeline, padding included. 'none' does not scroll. Anything else is a date: '2026-06-01', a Date or epoch milliseconds, aligned by scroll.align like today and the first task.
  • A fixed timeline is clamped. The chart cannot scroll past its ends, so 'today' for a plan that is entirely in the past or the future lands on the nearest end of it. Put timeline.includeToday: true in the options to make the range of the chart contain today, or turn on the infinite timeline, which grows to reach any date.
  • A call can bring its own align and behavior, as in gantt.scrollToDate('2026-06-01', { align: 'end', behavior: 'instant' }). scrollToTask(id, { alignY }) places the row independently of the bar, and expands the collapsed parents of the row first (expand: false does not).
  • Calls made too early (before the chart has a width) are remembered, the latest one wins, and they are performed at mount instead of scroll.initial.
  • The dates are wall-clock Dates. getVisibleRange() and the date of onScroll are Dates that read the same in every time zone, so format them with the local formatter, as helpers.ts does. now is floating wall-clock time too.
  • Why on('scroll') in the vanilla code. The first scroll of a chart, the one that applies scroll.initial, happens inside createGantt, before the function has returned the chart. An onScroll option that reads the chart would run too early, so the example adds the listener with gantt.on('scroll', …) once it has the chart. React has no such trouble: the handle in the ref is set before the first event.