Theming

Import the stylesheet, follow your app's light and dark mode, and override the color tokens.

The chart takes its colors from CSS custom properties in a stylesheet your app imports. It follows your app's light and dark mode with no prop set.

Load the stylesheet

Import the stylesheet once, anywhere in your app. The component alone pulls in no styles.

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-03-02', endDate: '2026-03-06' },
  { id: 'build', name: 'Build', parentId: null, sequence: '2',
    startDate: '2026-03-09', endDate: '2026-03-20', progress: 40 },
];

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

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

Without the stylesheet the chart mounts and responds to clicks, but it is unreadable. The file carries the token declarations, the colors, the sticky headers, the bar geometry, the container's flex column and the box-sizing: border-box the layout math assumes.

Those rules are scoped to .gantt-container and its descendants, so the library adds no global reset.

Follow your app's theme

theme is optional, and omitting it is not the same as theme="light". With no prop the chart follows the page's color-scheme, read from <html>.

app.css
:root { color-scheme: light; }
.dark { color-scheme: dark; }

color-scheme is the platform's own light and dark signal, and it inherits. next-themes and the Tailwind and fumadocs setups built on it already write it, so a chart with no theme follows your app's toggle with no extra wiring.

A MutationObserver watches <html> for class, style and data-theme, so a switch repaints the chart on the next render, with no reload or remount. Only <html> is read, so a value on <body> or a wrapper does not reach the chart.

Pass 'light' or 'dark' to pin one chart, or 'system' to track the OS inside an app whose own theme is fixed. A page whose color-scheme is light dark hands the choice to the OS as well, and a page that never sets it computes to normal, which reads as light.

On the server and the first hydration render, 'system' and an omitted prop both resolve to light.

The container always carries a resolved data-theme, and the stylesheet reads only that attribute. className is appended and carries no theme meaning, so a class named dark does not conflict.

The demo above is pinned to theme="dark", so the site's own theme switch does not move it.

The custom properties

All 26 tokens are declared on .gantt-container, not on :root, where they would collide with your app's own --background and --border. The dark block redefines 20 of them, and the 6 marked same are shared.

The stylesheet also sets color-scheme on the container, so the browser paints its scrollbars and caret to match.

TokenLightDarkPaints
--gantt-background#fafafa#09090bcontainer, panes, headers, detail panel
--gantt-foreground#18181b#fafafabody text and grid cells
--gantt-muted#f4f4f5#18181bbottom header row, row hover and selection
--gantt-muted-foreground#71717a#a1a1aasecondary text, resize and row grips
--gantt-border#e4e4e7#27272aheader and grid borders, scrollbar thumb
--gantt-border-subtlergba(0, 0, 0, 0.04)rgba(255, 255, 255, 0.04)row separators
--gantt-accent#3b82f6samefocus rings, selection, link preview
--gantt-bar-bg#c7c7cd#3a3a41a task bar
--gantt-bar-bg-hover#b4b4bc#47474fthe same bar on hover
--gantt-bar-text#18181b#fafafathe task name inside a bar
--gantt-bar-shadownonenonea bar at rest
--gantt-bar-shadow-hover0 1px 2px rgba(0, 0, 0, 0.08)0 1px 2px rgba(0, 0, 0, 0.4)a hovered bar
--gantt-bar-shadow-drag0 4px 12px rgba(0, 0, 0, 0.16)0 4px 12px rgba(0, 0, 0, 0.5)a dragged bar
--gantt-shadow-overlay0 4px 16px rgba(0, 0, 0, 0.15)0 8px 24px rgba(0, 0, 0, 0.55) plus a 1px ringthe bar tooltip
--gantt-summary-bg#dcdce0#2a2a2fa summary row's bar
--gantt-summary-bg-hover#cfcfd4#35353cthe same bar on hover
--gantt-progress-bg#8e8e97#6c6c76the progress fill on a task bar
--gantt-progress-handle#52525b#a1a1aathe progress handle border
--gantt-arrow#a1a1aa#71717aarrow stroke and arrowhead, at 45% opacity at rest
--gantt-today-marker#f43f5e#fb7185the today line and rejected states
--gantt-non-working-bgrgba(0, 0, 0, 0.035)rgba(255, 255, 255, 0.03)non-working columns
--gantt-detail-width320pxsamethe docked detail panel
--gantt-font-sansPretendard, then the system stacksamethe container's font-family
--gantt-duration-fast150mssamethe tooltip fade-in
--gantt-transition-fast150ms easesamebar background, chevron, grips, handles
--gantt-transition-normal200ms easesamea bar's box-shadow

A summary row's roll-up fill is --gantt-foreground at 25% opacity, so --gantt-progress-bg does not reach it. A dependency arrow goes opaque when it or either of its tasks is hovered or selected. It takes --gantt-accent in place of --gantt-arrow only when the arrow itself is hovered or selected.

showNonWorkingDays, workingWeekdays and holidays decide which days the shading covers, as the timeline describes. A task's color is read before the bar tokens, so a colored task ignores them. See Styling.

No font is bundled or fetched. The stylesheet names Pretendard and falls through to the system font, so serving it is your app's job. The base size is 13px and is not a token.

Under prefers-reduced-motion: reduce every transition and animation inside the container is forced to 0.01ms and scroll-behavior to auto, whatever the motion tokens say. See Keyboard and screen readers.

Override a token

Write the override on .gantt-container and load your stylesheet after the library's. One rule is enough for the 6 tokens the dark block leaves alone. The other 20 take two rules, because a plain .gantt-container rule at specificity (0,1,0) loses to the library's .gantt-container[data-theme="dark"] at (0,2,0) and would apply in light mode only.

app.css
.gantt-container {
  --gantt-bar-bg: #dbeafe;
  --gantt-bar-bg-hover: #bfdbfe;
}

.gantt-container[data-theme="dark"] {
  --gantt-bar-bg: #1e3a5f;
  --gantt-bar-bg-hover: #2b4f7d;
}

Setting the tokens on :root has no effect. They are declared on the container itself, and a declaration on the element beats an inherited value.

Source order settles a tie only between rules in the same cascade layer. Import the library's stylesheet into a layer and layer order decides instead, so an unlayered rule of yours wins.

Restyle one chart

Pass a className and add it to every selector, including the [data-theme="dark"] one. Each qualified selector carries one more class than the library's, so it wins.

app.css
.gantt-container.planning-chart {
  --gantt-bar-bg: #dbeafe;
}

.gantt-container.planning-chart[data-theme="dark"] {
  --gantt-bar-bg: #1e3a5f;
}

Before you integrate

  • GanttTheme is the union 'light' | 'dark' | 'system'. There is no tokens prop, theme object, exported palette or theme provider.
  • theme is not validated. An unknown value produces no warning and behaves like an omitted prop.
  • Geometry is not tokenized. --gantt-row-height is written inline from a JS constant, so a stylesheet override loses to it.
  • The detail panel's slide is a plain 200ms the motion tokens do not drive, and the dependency delete glyph's stroke is hardcoded white, the only color a token cannot reach.
  • RTL is not supported. Nothing reads or sets dir, and the stylesheet uses physical properties throughout.

Props and defaults: GanttProps.