Skip to content

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]);

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, or flex: 1; min-height: 0 inside a column flex layout (without min-height: 0 a flex child may refuse to shrink and push the page instead), or height: 100% inside a parent that has a height.
  • React. <Gantt style={{ height: 420 }} /> or a className that sets a height. <Gantt> renders a single div and applies both.
  • The layout.height option. 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.

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'; // vanilla
import '@ganttlane/react/styles.css'; // React

Cause 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.

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.css and @ganttlane/react/styles.css ship an (empty) declaration file through the types condition of their subpath, so the import type-checks as it is. If you still see TS2882 for them, TypeScript is not reading the package’s exports: 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.ts of your project (or "types": ["vite/client"] in tsconfig.json); elsewhere, a declare module '*.css'; in a .d.ts does it.
  • TypeScript must resolve package exports. The packages are ESM only, with exports and declarations. Set "moduleResolution": "bundler" (for an app compiled by a bundler) or "node16" / "nodenext". The old node10 / classic resolution cannot see subpaths such as @ganttlane/dom/styles.css or @ganttlane/react/core.

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.

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 with Z and is rejected (strings with Z or 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 day
start: '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 0

If 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.

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 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.

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.

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.

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.