Skip to content

Task colours and classes

There are two ways to say what a task looks like, and the chart supports both:

  • A class, task.className for the bar and task.rowClassName for its row. The class is a hook for your stylesheet: the example’s styles.css turns five status classes into five bar colours, and two of them into a tint of the whole row. Themes, hover states and print styles all stay in CSS.
  • A colour in the data, task.color and task.progressColor. The renderer writes them as the custom properties --gantt-bar-color and --gantt-bar-progress-color on that one bar. Nothing to write in CSS; the colour is a value you compute.

The plan below is styled the first way. The Style from select swaps the tasks for the same plan with colours in the data. Row stripes is display.rowStripes.

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';
/** What `task.data` holds: the status of a task. */
export interface Meta {
status: 'done' | 'active' | 'late' | 'blocked' | 'review';
}
// 2026-03-02 is a Monday. Three phases with children, one status each, styled by CLASS: the
// stylesheet (`styles.css`) turns `className` into the colour of the bar and `rowClassName` into a
// tint of the whole row. A phase is a task like the others, with children.
export const tasks: Task<Meta>[] = [
// ---- Discovery: finished ----
{
id: 'discovery',
name: 'Discovery',
start: '2026-03-02',
end: '2026-03-06',
progress: 100,
className: 'is-phase is-done',
data: { status: 'done' },
},
{
id: 'interviews',
name: 'Interviews',
parentId: 'discovery',
start: '2026-03-02',
end: '2026-03-03',
progress: 100,
className: 'is-done',
data: { status: 'done' },
},
{
id: 'requirements',
name: 'Requirements',
parentId: 'discovery',
start: '2026-03-04',
end: '2026-03-06',
progress: 100,
className: 'is-done',
data: { status: 'done' },
},
// ---- Build: one task is late, one is blocked; their rows are tinted ----
{
id: 'build',
name: 'Build',
start: '2026-03-09',
end: '2026-03-24',
progress: 45,
className: 'is-phase is-active',
data: { status: 'active' },
},
{
id: 'backend',
name: 'Backend',
parentId: 'build',
start: '2026-03-09',
end: '2026-03-18',
progress: 70,
className: 'is-active',
data: { status: 'active' },
},
{
id: 'frontend',
name: 'Frontend',
parentId: 'build',
start: '2026-03-11',
end: '2026-03-24',
progress: 35,
className: 'is-late',
rowClassName: 'row-late',
data: { status: 'late' },
},
{
id: 'integrations',
name: 'Integrations',
parentId: 'build',
start: '2026-03-19',
end: '2026-03-24',
progress: 0,
className: 'is-blocked',
rowClassName: 'row-blocked',
data: { status: 'blocked' },
},
// ---- Release ----
{
id: 'release',
name: 'Release',
start: '2026-03-25',
end: '2026-03-27',
progress: 0,
className: 'is-phase is-review',
data: { status: 'review' },
},
{
id: 'qa',
name: 'QA',
parentId: 'release',
start: '2026-03-25',
end: '2026-03-26',
progress: 50,
// Colour set on the task itself wins over the class: this one bar is teal whatever its
// status says, and its progress is a darker teal of its own.
color: '#0f9d8a',
progressColor: '#0a6b5d',
className: 'is-active',
data: { status: 'active' },
},
{
id: 'launch',
name: 'Launch',
parentId: 'release',
type: 'milestone',
start: '2026-03-27',
className: 'is-review',
data: { status: 'review' },
},
];
export const links: Link[] = [
{ source: 'interviews', target: 'requirements' },
{ source: 'requirements', target: 'backend' },
{ source: 'backend', target: 'integrations' },
{ source: 'integrations', target: 'qa' },
{ source: 'qa', target: 'launch' },
];
/** The colours of the statuses when they come from the data, not from a stylesheet. */
const COLOURS: Record<Meta['status'], { color: string; progressColor: string }> = {
done: { color: '#2f9e44', progressColor: '#1c6b2c' },
active: { color: '#3f6fd1', progressColor: '#264a96' },
late: { color: '#d9480f', progressColor: '#9c3207' },
blocked: { color: '#6b7280', progressColor: '#3f4651' },
review: { color: '#7c3aed', progressColor: '#5421ab' },
};
/**
* The same plan styled from the data instead: every task carries its own `color` and
* `progressColor` (the renderer writes them as `--gantt-bar-color` and `--gantt-bar-progress-color`
* on its bar), and no class. A colour is a value, so there is no theme to follow and no rule to
* write, but there is also no row tint: `rowClassName` is the only way to style a row. (QA keeps
* the teal it sets for itself.)
*/
export const coloured: Task<Meta>[] = tasks.map(({ className, rowClassName, ...task }) => ({
...COLOURS[task.data?.status ?? 'active'],
...task,
}));

Files

  • data.ts: the plan with className and rowClassName (tasks), and coloured, the same plan with color and progressColor. A Task<Meta> carries its status in data.
  • options.ts: display.rowStripes, row and bar heights, the first scroll.
  • styles.css: the status classes (one custom property each) and the row tints.
  • vanilla.ts and react.tsx: the two selects. The tasks are swapped with setTasks, the stripes with setOptions({ display }) (React: the display prop).

Options

Option Value What it does
task.className 'is-done', 'is-phase is-active', … Added to the class of the bar (g.gantt__bar); several classes separated by spaces
task.rowClassName 'row-late', 'row-blocked' Added to the class of the task’s row (rect.gantt__row)
task.color, task.progressColor '#0f9d8a', '#0a6b5d' (QA; all in coloured) --gantt-bar-color and --gantt-bar-progress-color on the bar
display.rowStripes 'group'; the select: 'row', false Zebra stripes: every other row, every other parent with its children, or none
layout.rowHeight, layout.barHeight 34, 20 Shorter rows than the default 48 and 30, so the whole plan is on the screen
viewMode 'week' The scale of the timeline (React: defaultViewMode)
scroll.initial 'first-task' Opens on the plan

What to try

Choose “colours in the data”: the bars keep their colours (they now come from color), but the red and grey row tints are gone, because a row can only be styled by a class. QA is teal in both modes: its own color and progressColor win over its class. Switch Row stripes between 'group', 'row' and false: with 'group' the stripes follow the phases, with 'row' they follow the rows. A tinted row paints over its stripe. Hover a bar and click an arrow, as in any chart: the hover outline, the selection and the conflict marks are the library’s and combine with your classes.

Notes

  • A class sets a custom property, not the paint. .gantt__bar.is-late { --gantt-bar-color: … } is enough: the track, the milestone diamond and the progress (the bar colour mixed towards black) all read it. Set --gantt-bar-progress-color in the class too if the progress should be another colour, and --gantt-bar-label-color for the label. Setting fill on .gantt__bar-track yourself also works, but the progress colour, which is derived from --gantt-bar-color, would not follow it.
  • Precedence. task.color is written on the bar element itself, so it beats a class; a class beats the theme’s --gantt-bar-color on .gantt, which the bar inherits. A rule of yours is not in @layer gantt, so it beats the library’s own rules without !important.
  • A row class paints the whole row, over the stripe and under the bars: use a colour with an alpha (rgb(217 72 15 / 10%)), so the grid lines and days off stay visible. There is no color counterpart for a row.
  • A phase is a task. Give a parent a class of its own (is-phase here: a darker progress) next to its status. Its dates are its own; the library does not roll them up from the children.
  • Dark theme. The status colours are mid-tones that work on both backgrounds. If they should differ, add .gantt[data-theme='dark'] .is-done { … } rules, or derive the colour from color-mix(); see Theming for how a theme is written.
  • Which to pick. Classes when the look is a design decision (a palette, a theme, a print sheet) or when you also want a row style. Colours when they are data (a label colour a user picked, a project’s colour) and not worth a rule each.