Highlighted days
The highlights option marks days of the timeline: an offsite, a code freeze, every Friday. A
highlight is for the eye only: it changes no date, no duration and no calendar, and a bar can
be dragged across it. What the calendar says is a different thing, drawn by a different option:
the weekends and the holidays are non-working days, and display.nonWorkingDays chooses how
they look. Both show a label when you hover a day, and the two are joined when they meet on one
day.
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';
// March 2026 (the 2nd is a Monday), a plan of eight tasks. The days marked in `options.ts` (an// offsite, two demo days, a code freeze, the Fridays and a holiday on Friday the 20th) fall in the// middle of it. No task starts or ends on the holiday: that would be a scheduling conflict, which// the chart draws on the bar.export const tasks: Task[] = [ { id: 'design', name: 'Design', start: '2026-03-02', end: '2026-03-06', progress: 100 }, { id: 'prototype', name: 'Prototype', start: '2026-03-09', end: '2026-03-13', progress: 100 }, { id: 'backend', name: 'Backend', start: '2026-03-09', end: '2026-03-19', progress: 60 }, { id: 'frontend', name: 'Frontend', start: '2026-03-16', end: '2026-03-27', progress: 30 }, { id: 'docs', name: 'Docs', start: '2026-03-16', end: '2026-03-27', progress: 20 }, { id: 'qa', name: 'QA', start: '2026-03-23', end: '2026-03-27' }, { id: 'review', name: 'Review', start: '2026-03-30', end: '2026-03-30' }, { id: 'release', name: 'Release', type: 'milestone', start: '2026-03-31' },];
export const links: Link[] = [ { source: 'design', target: 'prototype' }, { source: 'design', target: 'backend' }, { source: 'prototype', target: 'frontend' }, { source: 'backend', target: 'qa' }, { source: 'qa', target: 'review' }, { source: 'frontend', target: 'review' }, { source: 'docs', target: 'review' }, { source: 'review', target: 'release' },];import type { GanttOptions, HighlightInput } from '@ganttlane/dom';
/** * Highlighted days are for the eye only: they change no date and no calendar. Each one has an `id` * and some days, from `dates`, a `range` (both ends included), a `match` predicate, or all three. */export const HIGHLIGHTS: HighlightInput[] = [ { id: 'offsite', // A run of days, both ends included. range: { start: '2026-03-09', end: '2026-03-11' }, // The text of the label that shows when you hover a day of it. label: 'Team offsite', // The colour of its days, as a CSS colour (`--gantt-highlight-color`). color: '#3b82f6', }, { id: 'demo', // Single days. dates: ['2026-03-04', '2026-03-18'], label: 'Demo day', color: '#2f9e44', }, { id: 'freeze', range: { start: '2026-03-23', end: '2026-03-26' }, label: 'Code freeze', color: '#e5484d', },];
/** A highlight that is a rule: every Friday. The select adds it to the list and takes it out. */export const FRIDAYS: HighlightInput = { id: 'retro', // Asked about every day on the screen; `weekday` is 0 for Sunday, so Friday is 5. match: (day) => day.weekday === 5, // A function gives each day its own label. label: (day) => `Retro, ${String(day.day)}/${String(day.month)}`, // An extra class on the days of this highlight, which `styles.css` makes paler. className: 'is-retro',};
/** What the select of non-working days offers, and the value each puts in `display.nonWorkingDays`. */export const NON_WORKING_DAYS = [ { label: "'fill'", value: 'fill' }, { label: "'stripes'", value: 'stripes' }, { label: 'false', value: false },] as const;
export const options = { viewMode: 'week', calendar: { // A day off with a name. Like the weekends it is a non-working day (a scheduling rule that // `display.nonWorkingDays` draws), and its name shows up on hover next to the highlights'. holidays: [{ date: '2026-03-20', name: 'Public holiday' }], }, // The list of highlighted days. A new list replaces the old one. highlights: [...HIGHLIGHTS, FRIDAYS], display: { // How non-working days (the weekends and the holidays) are drawn: 'fill' (the default), // 'stripes' or `false` for not at all. nonWorkingDays: 'fill', // Whether the highlighted days are drawn at all (the default). highlights: true, }, // The label of a highlighted or holiday day stays where it opened (the default), or follows // the cursor. highlightTooltip: { followCursor: false }, scroll: { initial: 'first-task' }, // Compact rows, so that every task fits without scrolling down. layout: { rowHeight: 32, barHeight: 20 },} satisfies Partial<GanttOptions>;import '@ganttlane/dom/styles.css';import './styles.css';
import { createGantt } from '@ganttlane/dom';
import { links, tasks } from './data';import { describe, type Settings } from './helpers';import { FRIDAYS, HIGHLIGHTS, NON_WORKING_DAYS, 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 = '340px'; const status = document.createElement('p'); root.append(toolbar, chart, status); container.append(root);
const gantt = createGantt(chart, { tasks, links, ...options });
let settings: Settings = { nonWorkingDays: options.display.nonWorkingDays, highlights: options.display.highlights, fridays: true, followCursor: options.highlightTooltip.followCursor, }; const show = (): void => { status.textContent = describe(settings); }; show();
const controller = new AbortController(); const { signal } = controller;
// The select of non-working days: `display` merges field by field with what was set before. const select = document.createElement('select'); NON_WORKING_DAYS.forEach(({ label }, index) => { const option = document.createElement('option'); option.value = String(index); option.textContent = label; select.append(option); }); select.addEventListener( 'change', () => { const choice = NON_WORKING_DAYS[Number(select.value)]; if (!choice) return; settings = { ...settings, nonWorkingDays: choice.value }; gantt.setOptions({ display: { nonWorkingDays: choice.value } }); show(); }, { signal }, ); const selectLabel = document.createElement('label'); selectLabel.append('Non-working days ', select); toolbar.append(selectLabel);
const addCheckbox = ( name: string, checked: boolean, onChange: (checked: boolean) => void, ): void => { const input = document.createElement('input'); input.type = 'checkbox'; input.checked = checked; input.addEventListener( 'change', () => { onChange(input.checked); show(); }, { signal }, ); const label = document.createElement('label'); label.append(input, ` ${name}`); toolbar.append(label); }; addCheckbox('Highlights', settings.highlights, (highlights) => { settings = { ...settings, highlights }; gantt.setOptions({ display: { highlights } }); }); addCheckbox('Fridays', settings.fridays, (fridays) => { settings = { ...settings, fridays }; // A list is replaced as a whole: this is the new list of highlights. gantt.setOptions({ highlights: fridays ? [...HIGHLIGHTS, FRIDAYS] : HIGHLIGHTS }); }); addCheckbox('Label follows the cursor', settings.followCursor, (followCursor) => { settings = { ...settings, followCursor }; gantt.setOptions({ highlightTooltip: { followCursor } }); });
return () => { controller.abort(); gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';import './styles.css';
import { Gantt } from '@ganttlane/react';import { useState } from 'react';
import { links, tasks } from './data';import { describe } from './helpers';import { FRIDAYS, HIGHLIGHTS, NON_WORKING_DAYS, options } from './options';
export default function Example() { const [nonWorking, setNonWorking] = useState(0); const [highlights, setHighlights] = useState<boolean>(options.display.highlights); const [fridays, setFridays] = useState(true); const [followCursor, setFollowCursor] = useState<boolean>(options.highlightTooltip.followCursor); const nonWorkingDays = NON_WORKING_DAYS[nonWorking]?.value ?? options.display.nonWorkingDays; // In React `viewMode` is a controlled prop, and `defaultViewMode` is its uncontrolled twin: here // nothing changes the view mode, so the chart owns it and starts from the option. const { viewMode, ...rest } = options;
return ( <> <div role="toolbar"> <label> Non-working days{' '} <select value={nonWorking} onChange={(event) => { setNonWorking(Number(event.target.value)); }} > {NON_WORKING_DAYS.map(({ label }, index) => ( <option key={label} value={index}> {label} </option> ))} </select> </label> <label> <input type="checkbox" checked={highlights} onChange={(event) => { setHighlights(event.target.checked); }} />{' '} Highlights </label> <label> <input type="checkbox" checked={fridays} onChange={(event) => { setFridays(event.target.checked); }} />{' '} Fridays </label> <label> <input type="checkbox" checked={followCursor} onChange={(event) => { setFollowCursor(event.target.checked); }} />{' '} Label follows the cursor </label> </div> <Gantt // The chart fills its container and scrolls inside it, so the container needs a height. style={{ height: 340 }} // The chart owns the data (uncontrolled): these are where it starts. defaultTasks={tasks} defaultLinks={links} defaultViewMode={viewMode} {...rest} // The same options as props: a new list or a changed field is applied with `setOptions`. highlights={fridays ? [...HIGHLIGHTS, FRIDAYS] : HIGHLIGHTS} display={{ nonWorkingDays, highlights }} highlightTooltip={{ followCursor }} /> <p>{describe({ nonWorkingDays, highlights, fridays, followCursor })}</p> </> );}/** What the controls of the example hold. */export interface Settings { /** `display.nonWorkingDays`. */ readonly nonWorkingDays: 'fill' | 'stripes' | false; /** `display.highlights`. */ readonly highlights: boolean; /** Whether the Fridays rule is in the `highlights` list. */ readonly fridays: boolean; /** `highlightTooltip.followCursor`. */ readonly followCursor: boolean;}
/** * Says which options the settings amount to, in the words of the options. * * @param settings - What the controls hold. * @returns A sentence. */export function describe(settings: Settings): string { const ids = ['offsite', 'demo', 'freeze', ...(settings.fridays ? ['retro'] : [])].join(', '); const nonWorking = settings.nonWorkingDays === false ? 'false' : `'${settings.nonWorkingDays}'`; return `display: { nonWorkingDays: ${nonWorking}, highlights: ${String(settings.highlights)} }, highlightTooltip: { followCursor: ${String(settings.followCursor)} }, highlights: [${ids}]`;}/* * Your own stylesheet: any stylesheet of the page, loaded after the library's. A highlight's * `className` is added to the class of each of its days (`rect.gantt__highlight`), and the theme * draws a day with `--gantt-highlight-color` at `--gantt-highlight-opacity`, so a custom property * on the class is all it takes. */.gantt__highlight.is-retro { --gantt-highlight-opacity: 0.1;}Files
data.ts: eight tasks in March, and their links.options.ts: the highlights, the calendar’s holiday,displayandhighlightTooltip.styles.css: one class, for the Fridays: your own stylesheet.helpers.ts:describe, which writes the status line from the settings.vanilla.tsandreact.tsx: the controls, withsetOptionsand props.
Options
| Option | Value | What it does |
|---|---|---|
highlights[].id |
'offsite', 'demo', 'freeze', 'retro' |
The name of a highlight (required, and unique); it is data-highlight on its days |
highlights[].range |
{ start, end } |
A run of days, both ends included |
highlights[].dates |
['2026-03-04', '2026-03-18'] |
Single days |
highlights[].match |
(day) => day.weekday === 5 |
A rule, asked about each day on the screen; weekday is 0 for Sunday |
highlights[].label |
a string, or (day) => string |
The text shown when a day is hovered |
highlights[].color |
'#3b82f6' |
The colour of its days (the CSS custom property --gantt-highlight-color) |
highlights[].className |
'is-retro' |
An extra class on its days, for your own stylesheet |
display.highlights |
true |
Whether highlighted days are drawn at all (the default) |
display.nonWorkingDays |
'fill', 'stripes' or false |
How the weekends and holidays are drawn: shaded (the default), hatched, or not at all |
highlightTooltip.followCursor |
false |
The label stays where it opened (the default), or follows the cursor |
calendar.holidays |
[{ date: '2026-03-20', name: 'Public holiday' }] |
A named day off: a non-working day, drawn like a weekend, whose name joins the label |
viewMode |
'week' |
The scale of the timeline (React: defaultViewMode, as nothing here changes it) |
scroll.initial |
'first-task' |
Opens on the plan |
layout.rowHeight, barHeight |
32, 20 |
Compact rows, so that every task fits |
Any mix of dates, range and match makes the days of one highlight. A range of years costs
only the days that are on the screen.
What to try
Hover the blue days (Team offsite), the green ones (Demo day) and the red ones (Code freeze).
Friday the 20th is a Friday and the public holiday, so its label joins the two: Retro, 20/3 ·
Public holiday. Switch Non-working days to 'stripes', and the weekends and the holiday are
hatched; to false, and they are not drawn at all (the calendar still treats them as days off).
Untick Highlights: every colour but the weekends goes. Untick Fridays: that is a new list
of highlights without the rule, handed to setOptions({ highlights }). Tick Label follows the
cursor and hover a day again.
Notes
- The list is replaced as a whole.
setOptions({ highlights })takes a new list (React: a newhighlightsprop), and the chart draws that. Fields ofdisplayare merged one by one:setOptions({ display: { nonWorkingDays: 'stripes' } })leavesdisplay.highlightsas it was. - Overlap. Highlights that share a day are all drawn, the later one in the list on top, and the label shown is the one of the highlight on top.
- A day label is joined from the highlight’s
labeland the calendar’s name for the day (holidays[].name), in that order, with a dot between them. A weekend has no name, so a weekend with no highlight shows nothing. - Bad input is reported, not thrown. A highlight without a string
id, or with anidan earlier one has, is dropped; a field of the wrong kind is dropped from its highlight. Each is aninvalid-optionissue athighlights[i].<field>, ingetIssues().optionsandonIssues. - Colours follow the theme. A highlight with no
colortakes--gantt-highlight-color, and the strength of every highlight is--gantt-highlight-opacity(lower in the dark theme). Theis-retroclass of this example turns it down for the Fridays; seestyles.css. - Days narrower than 2 px are not drawn. In the year view (1 px a day) neither highlights nor non-working days appear; the month view (4 px a day) and everything above it show them.
- Highlights are not holidays. To forbid work on a day, put it in
calendar.holidaysorcalendar.weekends(see Working days at one-hour granularity); to just mark it, put it inhighlights.