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.
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>.
: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.
| Token | Light | Dark | Paints |
|---|---|---|---|
--gantt-background | #fafafa | #09090b | container, panes, headers, detail panel |
--gantt-foreground | #18181b | #fafafa | body text and grid cells |
--gantt-muted | #f4f4f5 | #18181b | bottom header row, row hover and selection |
--gantt-muted-foreground | #71717a | #a1a1aa | secondary text, resize and row grips |
--gantt-border | #e4e4e7 | #27272a | header and grid borders, scrollbar thumb |
--gantt-border-subtle | rgba(0, 0, 0, 0.04) | rgba(255, 255, 255, 0.04) | row separators |
--gantt-accent | #3b82f6 | same | focus rings, selection, link preview |
--gantt-bar-bg | #c7c7cd | #3a3a41 | a task bar |
--gantt-bar-bg-hover | #b4b4bc | #47474f | the same bar on hover |
--gantt-bar-text | #18181b | #fafafa | the task name inside a bar |
--gantt-bar-shadow | none | none | a bar at rest |
--gantt-bar-shadow-hover | 0 1px 2px rgba(0, 0, 0, 0.08) | 0 1px 2px rgba(0, 0, 0, 0.4) | a hovered bar |
--gantt-bar-shadow-drag | 0 4px 12px rgba(0, 0, 0, 0.16) | 0 4px 12px rgba(0, 0, 0, 0.5) | a dragged bar |
--gantt-shadow-overlay | 0 4px 16px rgba(0, 0, 0, 0.15) | 0 8px 24px rgba(0, 0, 0, 0.55) plus a 1px ring | the bar tooltip |
--gantt-summary-bg | #dcdce0 | #2a2a2f | a summary row's bar |
--gantt-summary-bg-hover | #cfcfd4 | #35353c | the same bar on hover |
--gantt-progress-bg | #8e8e97 | #6c6c76 | the progress fill on a task bar |
--gantt-progress-handle | #52525b | #a1a1aa | the progress handle border |
--gantt-arrow | #a1a1aa | #71717a | arrow stroke and arrowhead, at 45% opacity at rest |
--gantt-today-marker | #f43f5e | #fb7185 | the today line and rejected states |
--gantt-non-working-bg | rgba(0, 0, 0, 0.035) | rgba(255, 255, 255, 0.03) | non-working columns |
--gantt-detail-width | 320px | same | the docked detail panel |
--gantt-font-sans | Pretendard, then the system stack | same | the container's font-family |
--gantt-duration-fast | 150ms | same | the tooltip fade-in |
--gantt-transition-fast | 150ms ease | same | bar background, chevron, grips, handles |
--gantt-transition-normal | 200ms ease | same | a 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.
.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.
.gantt-container.planning-chart {
--gantt-bar-bg: #dbeafe;
}
.gantt-container.planning-chart[data-theme="dark"] {
--gantt-bar-bg: #1e3a5f;
}Before you integrate
GanttThemeis the union'light' | 'dark' | 'system'. There is notokensprop, theme object, exported palette or theme provider.themeis not validated. An unknown value produces no warning and behaves like an omitted prop.- Geometry is not tokenized.
--gantt-row-heightis written inline from a JS constant, so a stylesheet override loses to it. - The detail panel's slide is a plain
200msthe 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.