View modes
A view mode is a zoom level of the timeline: the unit of its columns, how wide a day is, the
header rows above the columns, the grid lines and the step a drag snaps to. There are seven
built-in ones, and one plan can be read at any of them. This plan has days of work and a launch
day planned hour by hour, so the launch-day tasks are slivers in the week view and real bars in
the hour view. The select calls setViewMode, the buttons call zoomTo, and the line under the
chart is written by onViewModeChange.
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-30 is a Monday. A bare date is a whole day (an `end` is the last day, inclusive); a date// with a time is a timed task. So the plan has two scales in it: days of work, and launch day// (Tuesday 14 April) hour by hour. The launch-day tasks are slivers in the week view and real bars// in the hour view: switch the view mode to see both.export const tasks: Task[] = [ { id: 'design', name: 'Design', start: '2026-03-30', end: '2026-04-01', progress: 100 }, { id: 'backend', name: 'Backend', start: '2026-04-02', end: '2026-04-08', progress: 100 }, { id: 'frontend', name: 'Frontend', start: '2026-04-02', end: '2026-04-09', progress: 80 }, { id: 'docs', name: 'Docs', start: '2026-04-06', end: '2026-04-17', progress: 40 }, { id: 'qa', name: 'QA', start: '2026-04-10', end: '2026-04-13', progress: 30 },
{ id: 'checks', name: 'Final checks', start: '2026-04-14T08:00', end: '2026-04-14T09:00' }, { id: 'deploy', name: 'Deploy', start: '2026-04-14T09:00', end: '2026-04-14T10:30' }, { id: 'smoke', name: 'Smoke tests', start: '2026-04-14T10:30', end: '2026-04-14T12:00' }, { id: 'announce', name: 'Announce', start: '2026-04-14T13:00', end: '2026-04-14T14:00' }, { id: 'launch', name: 'Launch', type: 'milestone', start: '2026-04-14T14:00' },
{ id: 'monitor', name: 'Monitor', start: '2026-04-15', end: '2026-04-24' }, { id: 'retro', name: 'Retrospective', start: '2026-04-27', end: '2026-04-28' },];
export const links: Link[] = [ { source: 'design', target: 'backend' }, { source: 'design', target: 'frontend' }, { source: 'backend', target: 'qa' }, { source: 'frontend', target: 'qa' }, { source: 'qa', target: 'checks' }, { source: 'checks', target: 'deploy' }, { source: 'deploy', target: 'smoke' }, { source: 'smoke', target: 'announce' }, { source: 'announce', target: 'launch' }, { source: 'launch', target: 'monitor' }, { source: 'monitor', target: 'retro' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { // The scale of the timeline, one of the seven presets: 'hour', 'quarter-day', 'half-day', 'day', // 'week', 'month' or 'year'. `setViewMode(id)` changes it later without recreating the chart. viewMode: 'week', scheduling: { // The step of the data. The hour preset snaps a drag to 15 minutes and the quarter-day and // half-day presets to one hour, but a snap step is never finer than the granularity: at the // default, '1d', every preset below the day would snap to a whole day. granularity: '15min', }, // Open on launch day, with 11:00 (the middle of its run sheet) in the middle of the chart. // Switching the view mode keeps the moment in the middle where it is // (`setViewMode(id, { keepScroll: false })` scrolls like at mount instead). scroll: { initial: '2026-04-14T11:00', align: 'center' }, // Compact rows, so that every task fits without scrolling down. layout: { rowHeight: 32, barHeight: 20 },} satisfies Partial<GanttOptions>;import '@ganttlane/dom/styles.css';
import { createGantt } from '@ganttlane/dom';
import { links, tasks } from './data';import { describe, VIEW_MODES, zoomed } from './helpers';import { options } from './options';
export function mount(container: HTMLElement): () => void { const root = document.createElement('div'); const toolbar = document.createElement('div'); toolbar.setAttribute('role', 'toolbar'); // The chart fills its container and scrolls inside it, so the container needs a height. const chart = document.createElement('div'); chart.style.height = '470px'; const status = document.createElement('p'); status.textContent = 'Pick a view mode, or zoom in and out.'; root.append(toolbar, chart, status); container.append(root);
const gantt = createGantt(chart, { tasks, links, ...options, // The view mode in use changed: the handler gets the `ViewMode` itself. onViewModeChange: (viewMode) => { status.textContent = describe(viewMode, viewMode.pxPerDay); }, });
const controller = new AbortController(); const { signal } = controller;
const select = document.createElement('select'); for (const id of VIEW_MODES) { const option = document.createElement('option'); option.value = id; option.textContent = id; select.append(option); } select.value = options.viewMode; select.addEventListener('change', () => gantt.setViewMode(select.value), { signal }); const label = document.createElement('label'); label.append('View mode ', select); toolbar.append(label);
// `zoomTo` sets the pixels per day of the view mode in use, keeping the date in the middle. // Another view mode starts again from its own preset. const addZoom = (name: string, factor: number): void => { const button = document.createElement('button'); button.type = 'button'; button.textContent = name; button.addEventListener( 'click', () => { gantt.zoomTo(zoomed(gantt.core.getSnapshot().timeline.scale.pxPerDay, factor)); const { viewMode, timeline } = gantt.core.getSnapshot(); status.textContent = describe(viewMode, timeline.scale.pxPerDay); }, { signal }, ); toolbar.append(button); }; addZoom('Zoom in', 1.25); addZoom('Zoom out', 0.8);
return () => { controller.abort(); gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';
import { Gantt, type GanttHandle } from '@ganttlane/react';import { useRef, useState } from 'react';
import { links, tasks } from './data';import { describe, VIEW_MODES, zoomed } from './helpers';import { options } from './options';
export default function Example() { const chart = useRef<GanttHandle>(null); // `viewMode` is controlled by its prop: the chart shows the one your state holds. const [viewMode, setViewMode] = useState<string>(options.viewMode); const [status, setStatus] = useState('Pick a view mode, or zoom in and out.');
// `zoomTo` sets the pixels per day of the view mode in use, keeping the date in the middle. // Another view mode starts again from its own preset. const zoom = (factor: number): void => { const gantt = chart.current; if (!gantt) return; gantt.zoomTo(zoomed(gantt.core.getSnapshot().timeline.scale.pxPerDay, factor)); const { viewMode: mode, timeline } = gantt.core.getSnapshot(); setStatus(describe(mode, timeline.scale.pxPerDay)); };
return ( <> <div role="toolbar"> <label> View mode{' '} <select value={viewMode} onChange={(event) => { setViewMode(event.target.value); }} > {VIEW_MODES.map((id) => ( <option key={id} value={id}> {id} </option> ))} </select> </label> <button type="button" onClick={() => zoom(1.25)}> Zoom in </button> <button type="button" onClick={() => zoom(0.8)}> Zoom out </button> </div> <Gantt ref={chart} // The chart fills its container and scrolls inside it, so the container needs a height. style={{ height: 470 }} // The chart owns the data (uncontrolled): these are where it starts. defaultTasks={tasks} defaultLinks={links} {...options} viewMode={viewMode} // The view mode in use changed: the handler gets the `ViewMode` itself. onViewModeChange={(mode) => { setStatus(describe(mode, mode.pxPerDay)); }} /> <p>{status}</p> </> );}import type { ViewMode } from '@ganttlane/dom';
/** The seven presets, by the id that `viewMode` and `setViewMode` take. */export const VIEW_MODES = [ 'hour', 'quarter-day', 'half-day', 'day', 'week', 'month', 'year',] as const;
/** * Says what a view mode is: its name, the zoom (pixels per day) and the default snap step of a drag. * * @param viewMode - The `ViewMode` that `onViewModeChange` passes, or the one the snapshot holds. * @param pxPerDay - The zoom now in use; it differs from `viewMode.pxPerDay` after `zoomTo`. * @returns A sentence. */export function describe(viewMode: ViewMode, pxPerDay: number): string { const zoom = pxPerDay === viewMode.pxPerDay ? `${String(pxPerDay)} px per day` : `${pxPerDay.toFixed(1)} px per day (zoomed from ${String(viewMode.pxPerDay)})`; const snap = typeof viewMode.snap === 'string' ? viewMode.snap : 'its own step'; return `${viewMode.label ?? viewMode.id}: ${zoom}, a drag snaps to ${snap}`;}
/** * The zoom a step of the buttons leads to. `zoomTo` takes any positive number of pixels per day; * the buttons stay between a hairline year and a very wide hour. * * @param pxPerDay - The zoom now in use. * @param factor - Above 1 to zoom in, below 1 to zoom out. * @returns The new zoom. */export function zoomed(pxPerDay: number, factor: number): number { return Math.min(Math.max(pxPerDay * factor, 0.5), 4800);}Files
data.ts: twelve tasks, five of them timed to the hour, and their links.options.ts: the view mode, the granularity, the first scroll and compact rows.helpers.ts: the ids of the presets,describe(the status line) andzoomed.vanilla.ts:setViewMode,zoomToandonViewModeChangeon aGantt.react.tsx:viewModeas a controlled prop, andzoomTothrough theref.
The presets
id |
One column | pxPerDay |
Default snap | Header rows | Padding |
|---|---|---|---|---|---|
hour |
1 hour | 960 | 15min |
day, hour | 7 days |
quarter-day |
6 hours | 160 | 1h |
day, 6-hour block | 7 days |
half-day |
12 hours | 80 | 1h |
day, 12-hour block | 14 days |
day |
1 day | 40 | 1d |
month, day | 14 days |
week |
1 week | 20 | 1d |
month, week (2–8) |
4 weeks |
month |
1 month | 4 | 1d |
year, month | 4 months |
year |
1 year | 1 | 1mo |
decade, year | 2 years |
pxPerDay is the zoom: how many pixels a day is wide. The snap is the step a bar moves, resizes
or changes progress by when it is dragged; scheduling.snap replaces it for every mode, and holding
Alt while dragging snaps to the data step instead. The padding is the timeline the chart adds
before the first task and after the last. The modes made of hours have minor grid lines as well
(every 15 minutes in hour, every hour in quarter-day and half-day), which are drawn only while
they are at least 6 px apart: in half-day, at 80 px a day, they are too close.
Options
| Option | Value | What it does |
|---|---|---|
viewMode |
'week' |
The preset in use; setViewMode(id) changes it later (React: a controlled prop) |
scheduling.granularity |
'15min' |
The data step. A snap step is never finer than it, so the hour presets need it |
scroll.initial |
'2026-04-14T11:00' |
Opens on launch day (a moment, not 'today', which is far from this plan) |
scroll.align |
'center' |
The moment lands in the middle of the chart (setViewMode then keeps the middle in place) |
layout.rowHeight |
32 |
Compact rows, so that all twelve tasks fit |
layout.barHeight |
20 |
The height of a bar inside its row |
onViewModeChange |
a function (in the example) | Called with the ViewMode that is now in use: its label, pxPerDay and snap |
What to try
Pick hour: launch day fills the chart, with a column per hour and a faint line every 15 minutes. Drag Deploy and it moves a quarter of an hour at a time; the tasks linked after it move along. Pick week and drag QA: it moves a day at a time, which is the snap of that mode. Pick year and the whole plan is a few pixels wide, in the middle of two years of padding. Zoom in and Zoom out change the pixels per day of the mode you are in (the line under the chart says so); picking another mode starts from that mode’s own preset again.
Notes
- The moment in the middle stays.
setViewMode(id)keeps the date at the centre of the chart where it is, so zooming in on launch day lands on launch day. With{ keepScroll: false }as its second argument it scrolls like at mount instead (scroll.initial). zoomTo(pxPerDay, { anchor })keeps the date underanchor(an x in the viewport, default the middle) in place. The zoom belongs to the view mode in use and is dropped when the mode changes.- An id the chart does not know falls back to
'day'and is reported as an option issue; a mode of your own is added throughviewModes(Custom view modes). onViewModeChangefires only when the mode itself changes, not afterzoomTo; read the zoom withgantt.core.getSnapshot().timeline.scale.pxPerDay, as this example does.- React.
viewModeis a controlled prop, so the select changes your state and the chart follows it;defaultViewModeis its uncontrolled twin.optionsis spread as props, and the example passesviewModeafter it, for it to win.