FAQ and troubleshooting
Most first problems have one of a few causes. Find the symptom below; each answer says what the
library does, why, and what to change. When nothing here fits, ask the chart: bad data and bad
options never throw, they become issues, so
gantt.getIssues() (or an onIssues handler) usually names the problem.
const issues = gantt.getIssues();console.table([...issues.data, ...issues.options]);The chart is empty, or has no height
Section titled “The chart is empty, or has no height”Symptom. Nothing is drawn, or the page shows a thin strip with a header and no rows.
Cause 1: the container has no height. The chart fills its container and scrolls inside it, so a
container whose height comes from its content (a plain div with no height) is zero pixels tall
and the chart has nothing to fill.
<!-- Wrong: no height. --><div id="chart"></div>
<!-- Right: the container has a height. --><div id="chart" style="height: 420px"></div>There are three ways to give the chart a height:
- CSS on the container.
height: 420px, orflex: 1; min-height: 0inside a column flex layout (withoutmin-height: 0a flex child may refuse to shrink and push the page instead), orheight: 100%inside a parent that has a height. - React.
<Gantt style={{ height: 420 }} />or aclassNamethat sets a height.<Gantt>renders a singledivand applies both. - The
layout.heightoption. A number of pixels (layout: { height: 420 }) gives the chart its own height whatever its container does.'auto'makes the chart as tall as its rows and lets the page scroll them; see the layout options and the layout and sizes example.
Cause 2: the stylesheet is not loaded. Without @ganttlane/dom/styles.css the chart has no
layout at all. See The styles are missing.
Cause 3: no tasks, or no valid tasks. A task with an invalid date is a row without a bar and
an issue. An empty tasks array draws an empty grid. Check getIssues(), and see
A task is a row without a bar.
Cause 4: the container was hidden or detached when the chart was created. The chart measures
its container when it is created, and follows its size afterwards. If the container is display: none (a closed tab or a collapsed panel) the chart has no width to draw on until it is shown; a
scrollToDate, scrollToToday or scrollToTask made meanwhile is kept and performed when the
first width arrives. Prefer creating the chart when its panel is first shown.
Cause 5: the container is not an element. createGantt throws a TypeError for anything that
is not an element (a selector string, or null from a querySelector that found nothing). It is
the one thing it throws for. The container should also be empty: the chart owns what is inside it.
The chart opens at today, far from my data
Section titled “The chart opens at today, far from my data”Symptom. The timeline is empty when the chart appears, and the tasks are somewhere to the left or the right.
Cause. scroll.initial defaults to 'today'. A plan from last year or next year has no tasks
at today, and the chart opens at the nearest end of its range, not on the plan.
Fix. Choose where the chart opens:
createGantt(container, { tasks, scroll: { initial: 'first-task' }, // or 'start', 'end', 'none', or a date: '2026-03-02'});'first-task' opens on the earliest task start (on today when there are no tasks), 'start' and
'end' put the start of the range at the left edge or its end at the right edge, 'none' leaves
the chart where it is, and a date opens on that date. In React, pass scroll={{ initial: 'first-task' }} (object props are compared by
content, so an inline object is fine). To move later, call scrollToTask(id), scrollToDate(date)
or scrollToToday(). See the scroll options and the
scrolling and today example.
The styles are missing
Section titled “The styles are missing”Symptom. The chart is an unstyled pile of boxes, or a build error says it cannot resolve
@ganttlane/dom/styles.css.
Cause 1: the stylesheet is never imported. The JavaScript never imports it for you, so that
bundlers, CSS-in-JS setups and plain <link> tags all work. Import it once per page, before the
chart is created:
import '@ganttlane/dom/styles.css'; // vanillaimport '@ganttlane/react/styles.css'; // ReactCause 2: strict pnpm. Strict pnpm (and Yarn Plug’n’Play) lets you import only the packages your
own package.json lists. An app that installed only @ganttlane/react cannot reach
@ganttlane/dom, which is the React package’s dependency, not the app’s own, so
import '@ganttlane/dom/styles.css' fails to resolve. @ganttlane/react ships a byte-for-byte copy
of the stylesheet for this reason: import @ganttlane/react/styles.css instead (one of the two,
never both). The same applies to types and values of the core: @ganttlane/dom and
@ganttlane/react re-export the types you meet around a chart, and @ganttlane/react re-exports
patchTasks. For anything deeper, add @ganttlane/core to your own dependencies, on the same
version.
Cause 3: the CSS is stripped as unused. A bundler that drops imports of side-effect-free
packages must keep CSS. Both packages declare "sideEffects": ["**/*.css"], so a bundler that
honours the field keeps the import. If you maintain your own sideEffects rules for node_modules,
make sure *.css is not excluded.
Cause 4: the stylesheet loads after the chart is created. The chart measures its container when
it is created. A stylesheet that is still loading gives it the unstyled layout to measure, and a
position that depends on the width (a date aligned to the middle of the viewport) is centred on the
wrong number. Have the stylesheet in effect before you mount: import it at the top of the entry
module, or put the <link> in the document head.
Your own CSS looks overridden, or does not apply. The stylesheet is in @layer gantt, and
declarations that are not in a layer beat layered ones whatever their specificity. Your plain CSS
wins over the chart’s without !important; CSS you put in a layer of your own competes by the
order in which the layers are declared. See Theming.
TypeScript 6 and CSS imports
Section titled “TypeScript 6 and CSS imports”Symptom. error TS2882: Cannot find module or type declarations for side-effect import of '...css'.
Cause. TypeScript 6 checks side-effect imports (noUncheckedSideEffectImports is on by
default), so an import 'file.css' needs a declaration TypeScript can find.
- The ganttlane stylesheets need nothing from you.
@ganttlane/dom/styles.cssand@ganttlane/react/styles.cssship an (empty) declaration file through thetypescondition of their subpath, so the import type-checks as it is. If you still see TS2882 for them, TypeScript is not reading the package’sexports: see the next paragraph. - Your own CSS files need a declaration, which has nothing to do with ganttlane. In a Vite
project add
/// <reference types="vite/client" />to a.d.tsof your project (or"types": ["vite/client"]intsconfig.json); elsewhere, adeclare module '*.css';in a.d.tsdoes it. - TypeScript must resolve package
exports. The packages are ESM only, withexportsand declarations. Set"moduleResolution": "bundler"(for an app compiled by a bundler) or"node16"/"nodenext". The oldnode10/classicresolution cannot see subpaths such as@ganttlane/dom/styles.cssor@ganttlane/react/core.
misaligned issues
Section titled “misaligned issues”Symptom. getIssues() has a misaligned entry, with a suggested value, and a task is
shorter or longer than you wrote.
Cause. Every date, duration and lag in the chart is a multiple of scheduling.granularity
('1d' by default). A task that starts at '2026-03-02T09:30' is off the step of a day, so the
chart reports it and aligns it, according to scheduling.alignInput; the issue’s suggested field
is the value it used. It is a warning, not a failure: the task is drawn.
Fix. Either change the data to match the step, or change the step to match the data:
createGantt(container, { tasks: [{ id: 'a', name: 'Standup', start: '2026-03-02T09:30', end: '2026-03-02T10:00' }], scheduling: { granularity: '15min' }, // '1h', '30min', '15min', ... a divisor of 24 hours});Date-only strings ('2026-03-02') are never misaligned. A time of day needs a granularity that
divides it. See Granularity and alignment for the values
of alignInput (including 'reject', which makes the task invalid instead of aligning it) and the
granularity and snapping example; the list of issue
codes is in Issues instead of exceptions.
Dates are shifted by a time zone
Section titled “Dates are shifted by a time zone”Symptom. A task starts a day early, or an hour off, for some viewers.
Cause. ganttlane has no time zones: dates are floating wall-clock time. What shifts them is a conversion that happens before the chart sees the date.
new Date('2026-03-02')is UTC midnight. In a time zone behind UTC (New York, for one) that is the evening of March 1 on the local clock, which is what the chart reads.date.toISOString()ends withZand is rejected (strings withZor an offset are, because they name an instant, not a wall-clock time).
Fix. Pass strings without an offset, or Dates built from local fields:
start: '2026-03-02'; // a daystart: '2026-03-02T09:30'; // a time (needs a granularity that divides 09:30)start: new Date(2026, 2, 2); // local midnight; the month is counted from 0If your server sends instants, decide which wall clock the plan lives on, convert once on the way
in, and pass the result as a string with no offset. Read dates back with local getters, never
toISOString(). See Dates and time.
Other questions
Section titled “Other questions”My drag snaps back (controlled data)
Section titled “My drag snaps back (controlled data)”With dataMode: 'controlled' (in React: a tasks prop) the chart does not own the tasks: a drag
produces a ChangeSet and the chart waits for you to hand back new tasks. If your onTasksChange
does not write the change into your state (patchTasks(tasks, changeSet)), the bars return to where
your data says they are. A rejected or slow save behaves the same way. See
Who owns the data and the
controlled data example.
My handler fails because gantt is undefined
Section titled “My handler fails because gantt is undefined”Symptom. ReferenceError: Cannot access 'gantt' before initialization, or
Cannot read properties of undefined, from an onRangeChange, onScroll or onIssues handler
that uses the chart, at the moment the page creates it.
Cause. A handler given as an option can run before createGantt returns: the chart reports
its first range and scroll position while it mounts, so onRangeChange and onScroll always do
(when the container has a width), and onIssues does when the first options or data have issues.
The variable the chart is assigned to is not set yet. In React the same events are held until the
first commit, so a handler there can use the ref.
Fix. Declare the variable first and guard the handler, or add the listener after the chart exists:
let gantt: Gantt | undefined;gantt = createGantt(container, { tasks, onRangeChange: ({ start }) => gantt?.scrollToDate(start), // guarded: it can run early});
// Or, for a listener that only needs the events from now on:gantt.on('rangeChange', ({ reason }) => console.log(reason));A listener added afterwards does not hear the events of creation. See Handlers that run during creation.
A --gantt-* token does nothing
Section titled “A --gantt-* token does nothing”A token set on the container is not inherited by the chart: the stylesheet declares every token on
.gantt, the element the chart adds inside the container, and a declaration on the element itself
beats an inherited value. Put it in a rule for .gantt (every chart) or a descendant of your own
class (.roadmap .gantt, one chart):
/* Does nothing: .roadmap is the container. */.roadmap { --gantt-bar-color: #0f766e;}
/* Works. */.roadmap .gantt { --gantt-bar-color: #0f766e;}See Theming.
A before-hook throws and nothing happens
Section titled “A before-hook throws and nothing happens”If an onBeforeTaskChange or onBeforeLinkCreate hook throws or rejects during a drag, the change
is cancelled, and with no error listener nothing is reported. Give the chart an onError handler
while you develop. Calls from code (updateTask, addLink) hand the error back in their result
instead. See Change sets and before-hooks.
I passed an option and nothing changed
Section titled “I passed an option and nothing changed”Unknown options are ignored: an old key such as view_mode or bar_height does nothing. The
development build says so with an unknown-option warning in getIssues().options and onIssues
(you need the development export condition, which Vite and webpack set in dev mode); the
production build does not. Check the spelling against Options, and see
Migration if the key comes from frappe-gantt: fromFrappeGanttOptions converts a
whole options object. A value of the wrong type is not
ignored: it is an option issue in getIssues().options, and the default is used.
A task is a row without a bar
Section titled “A task is a row without a bar”Three things give a row without a bar. A task with no dates at all becomes a placeholder and gets a
no-dates warning. A task with only one of start, end and duration, with an end before its
start, or with an unreadable date is invalid (incomplete-dates, end-before-start, …). And
type: 'placeholder' asks for it, with no warning. TaskRecord.issues and getIssues().data say
which; see Tasks and records.
Should I use <Gantt> or useGantt in React?
Section titled “Should I use <Gantt> or useGantt in React?”<Gantt> for a chart; useGantt when you need the instance next to a chart of your own markup,
or useGanttState to read the chart’s state in another component. See React.
Related
Section titled “Related”- Issues instead of exceptions: every issue code and where it is delivered.
- Options: the
layout,scrollandschedulinggroups mentioned above. - Installation: requirements, the stylesheet and strict pnpm.
- Examples: layout and sizes, scrolling and today, issues, controlled data.