@lilydesignsystem/vue-gantt-chart

Lily Design System Vue 3 headless Gantt chart (column-span task bars over GanttTable, keyboard-accessible editing via date-time-picker)
985 3
Install
npm install @lilydesignsystem/vue-gantt-chart
Latest Version:0.8.0
Vue:3
License:MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause
Last Updated:Oct 25, 2026
Links: GitHub  ·  npm
Maintainer: joelparkerhenderson

GanttChart (Vue helper)

A reusable Vue 3 headless interactive Gantt chart. It composes @lilydesignsystem/vue-headless's GanttTable family — task bars rendered as column-spanning grid cells, never pixel-positioned floating divs — and @lilydesignsystem/vue-date-time-picker, used twice per edit session (start date, end date), as the keyboard-accessible way to reschedule or resize a task. Ships no CSS: every visual detail is the consumer's, via kebab-case class hooks and data-* attributes.

Editing is never drag-only. Native drag-and-drop fails WCAG 2.5.7 (Dragging Movements); the research behind this package (see spec/index.md) found that even mature commercial Gantt libraries ship no keyboard shortcut for dragging a bar — their accessible path is a typed-field edit surface, which is exactly what composing date-time-picker gives this package for free.

Usage

<script setup lang="ts">
import { ref } from "vue";
import GanttChart from "@lilydesignsystem/vue-gantt-chart";
import type { GanttTask } from "@lilydesignsystem/vue-gantt-chart";

const tasks = ref<GanttTask[]>([
  { id: "design", label: "Design", start: "2026-10-01", end: "2026-10-05" },
  { id: "build", label: "Build", start: "2026-10-06", end: "2026-10-12", dependsOn: ["design"] },
  { id: "launch", label: "Launch", start: "2026-10-13", end: "2026-10-13" }, // milestone
]);

function handleTaskChange(taskId: string, start: string, end: string) {
  tasks.value = tasks.value.map((t) => (t.id === taskId ? { ...t, start, end } : t));
}
</script>

<template>
  <GanttChart
    label="Q4 launch plan"
    :range="{ start: '2026-10-01', end: '2026-10-31' }"
    :tasks="tasks"
    today="2026-10-08"
    @task-change="handleTaskChange"
    :labels="{
      columnLabel: (start) => start,
      startLabel: 'Start date',
      endLabel: 'End date',
      saveLabel: 'Save',
      cancelLabel: 'Cancel',
      dependencySummary: (predecessors) => `Blocked by: ${predecessors.join(', ')}`,
      dateAnnouncement: (title, start, end) => `${title} rescheduled to ${start} – ${end}`,
      collapseButton: (task, collapsed) => (collapsed ? `Expand ${task.label}` : `Collapse ${task.label}`),
      dateTimePickerLabels: {
        previousYear: 'Previous year', previousMonth: 'Previous month',
        previousWeek: 'Previous week', previousDay: 'Previous day',
        nextDay: 'Next day', nextWeek: 'Next week',
        nextMonth: 'Next month', nextYear: 'Next year',
        confirm: 'Confirm', cancel: 'Cancel',
      },
    }"
  />
</template>

Props

See spec/index.md §5 for the full table. Required: label, range, tasks. Every optional labels.* field gates the control it names — editing itself is gated on labels.dateTimePickerLabels being supplied, since date-time-picker requires it too.

Events

  • task-change(taskId, start, end) — fires after a task's start/end changes, by pointer or by the edit region.

Behaviour

  • Time axis: range/timeUnit ("day" default, or "week"/ "month") generate a fixed set of columns using UTC/epoch-day arithmetic — never local-midnight Date construction.
  • Task bars: a [start, end] range marks every overlapping column's cell data-in-range; a milestone (start === end) marks one cell data-milestone.
  • Row hierarchy: task.parentId builds a tree; a parent's own start/end are derived from its descendants and rendered read-only, with a collapse button that removes descendant rows from the DOM outright.
  • Dependencies: task.dependsOn renders as an aria-describedby text summary; with dependencyArrows an aria-hidden SVG overlay also draws an arrow per dependency (0.6.0), so the text stays the accessible description.
  • Edit — keyboard: Enter/Space on a focused task row opens an inline region with two composed DateTimePicker instances.
  • Edit — pointer: native HTML5 drag-and-drop reschedules a task; supplementary, never the only path.
  • Keyboard: WAI-ARIA APG Grid roving-tabindex, the same model data-grid and kanban-board use.
  • Announcements: every successful edit announces via one aria-live="polite" region.
  • Typed fields and editing (0.5.0): fields + task.values add typed columns; field-edit edits them in place (click, F2, Tab). See spec §11.
  • Export and virtualization (0.5.0): labels.exportCsv / exportTsv / exportExcel plus labels.exportHeaders add CSV, TSV and Excel buttons; virtualize windows long task lists.

Weekend shading and dependency arrows (0.6.0)

  • weekends (weekdays, 0 = Sunday … 6 = Saturday, never defaulted) marks day columns data-weekend on the header and every cell; the 57 themes/ tint them. Week and month columns are not marked.
  • dependencyArrows draws an aria-hidden SVG arrow (.gantt-chart-dependency-arrow, data-from, data-to, data-dependency-type) per dependency by its type (e2s, s2s, s2e, e2e), measured from the rendered cells; the text description (labels.dependencySummary) remains. See spec §17.

Filtering and sorting (0.7.0)

  • filter(task) hides tasks it rejects; a passing task keeps its ancestors, as a search match does, and it combines with search (both must pass).
  • compare(a, b) orders tasks among their siblings — stable, and the hierarchy is kept (a child stays under its parent). Collapse, the visible export and keyboard order all follow it.

Export: copy to clipboard (0.7.0)

  • Copy to clipboard: labels.copy names a button (data-…-export-format="copy"; for the kanban board, Gantt chart and calendar it also needs labels.exportHeaders, like the downloads) that puts exactly what a download would hold — the same tasks, scope and order, header row first — on the clipboard as tab-separated text (toClipboardText: no byte-order mark, formula text defused), ready to paste into a spreadsheet. copyToClipboard uses the async Clipboard API and falls back to a hidden, selected <textarea> with execCommand("copy"); it never throws. labels.copyAnnouncement(rowCount) is announced, labels.copyFailed when neither path worked, and the copy event and copyHandler({ text, rowCount }) may return false to take over (nothing is copied).

Keyboard: page keys and type-ahead (0.7.0)

  • PageUp / PageDown move the cursor ten task rows (a virtualize page when virtualized), clamping at the ends.
  • Type-ahead: letters typed in quick succession move to the next task whose label starts with them (the label is taskLabel(task), ignoring case and diacritics; the same letter again steps through matches). A lone s stays the select shortcut; it joins a prefix already in progress. A Ctrl, Alt or Meta key, and an unmatched prefix, do nothing.

In-place edit: validation, undo and redo (0.7.0)

  • Field validate(value, values) refuses a value of the right type that is not acceptable (a negative estimate, an end before the start): return the reason. The editor stays open with aria-invalid="true" (cleared as soon as the user types again) and labels.editRefused(task, header, reason) is announced, falling back to labels.editInvalid.
  • Undo and redo: every accepted edit is remembered (the latest 100). Ctrl/Cmd+Z puts the earlier value back and Ctrl/Cmd+Shift+Z (or Ctrl+Y) re-applies one, both through the field-edit event with the restored value and the one it replaced — the consumer owns the data, so an undo is reported like an edit, and an undo is not itself recorded. A new edit ends the redo chain. Inside a field editor the browser's own undo is left alone; nothing happens in a readonly component or without the field-edit event. labels.undoAnnouncement(task, header, text) / labels.redoAnnouncement(task, header, text) are announced. The stacks are the pure edit-history.ts (recordEdit, undoEdit, redoEdit, historyKey).

Weekend, holiday and time-off indicators (0.7.0)

  • unavailable is a list of dated ranges { start, end?, kind?, label? } for holidays, time off and closures (end defaults to start; Lily ships no calendar, no kinds and no English). A column every day of which is covered gets data-unavailable="<kinds>" (the entries' kinds, unavailable when none, repeats removed) on its header and every cell; a column only partly covered gets data-partly-unavailable instead, so a holiday inside a week column is still marked. A label is rendered as text in the header (.gantt-chart-unavailable-label), so a day is never told apart by colour alone (WCAG 1.4.1). The weekend is weekends; markers flag one date; isNonWorking is a callback. Matching is the pure unavailable.ts (unavailableOn, unavailableIn, unavailableKinds), time-zone free.
  • Shading is CSS and ships in the 57 reference themes: weekends are tinted, unavailable days are tinted and striped, a partly unavailable column gets a bar under its header.

Direction and labels (0.7.0)

  • Right-to-left. The horizontal arrows are visual: in a right-to-left layout the later date is on the left, so ArrowLeft goes to the next day, and Alt+ArrowLeft/Right shifts a task (and Alt+Shift+Arrow moves only its end) the way the arrow points. Vertical keys, Home, End, page keys and type-ahead are unchanged. isRtl / logicalKey are exported.
  • No English left over. A complete Chinese bundle (labels, tasks, markers, unavailable days) is tested to leave no English text or aria-label in the DOM; every string is in labels.
  • Tests §18.43–§18.48.

Search (0.7.0)

search (a v-model; the field renders when labels.search names it) narrows the tasks: every word must appear, in any order, ignoring case and diacritics. New: the Gantt had no search. Matching tasks show with their ancestors (so the hierarchy stays readable); searching ignores collapse so a match under a collapsed parent is found, and clearing the search restores it. The default text is the task label and every field's text (searchText replaces it). Matches in the label are wrapped in <mark class="gantt-chart-match">; labels.searchAnnouncement(matches, total) is announced; Escape clears. exportScope: "visible" exports only what the search leaves. See spec §18.

Critical path (optional helper)

The chart renders critical and slack; it computes nothing. applyCriticalPath(tasks) (or criticalPath(tasks) for the raw result) fills them in: a forward and backward pass over leaf tasks, all four dependency kinds (e2s, s2s, e2e, s2e) with lags and leads in calendar days. A task's own start is a floor, so dependencies only push tasks later; summary rows, unscheduled tasks and unknown ids are ignored; a dependency cycle is reported in cycle and left out. Show it with labels.criticalLabel.

Lag is in calendar days by default. For schedules that count business days, pass { lagUnit: "working-days", weekends, unavailable } (the same weekends and unavailable you give the chart for shading): a lag of N then moves N working days, skipping weekends (default Saturday and Sunday) and the listed holidays, and the backward pass keeps a predecessor off non-working days so a weekend is never reported as slack. Task spans stay as dated; only the gaps between tasks change.

import { applyCriticalPath } from "@lilydesignsystem/vue-gantt-chart";
const tasks = applyCriticalPath(rawTasks); // each leaf task gains critical / slack

Non-goals (v1)

Critical-path calculation, interactive zoom-level switching, holiday shading from a calendar (isNonWorking marks them; weekends marks weekdays), resource/assignee columns. See spec/index.md §9 for why each is out.


Lily™ and Lily Design System™ are trademarks.

Related Packages

@lilydesignsystem/vue-kanban-board

Lily Design System Vue 3 headless kanban board (pointer + keyboard-accessible ca...

0 3
@lilydesignsystem/vue-data-grid

Lily Design System Vue 3 headless interactive data grid (sort, filter, selection...

335 3
@lilydesignsystem/vue-headless

A comprehensive headless Vue 3 component library with 491 accessible, unstyled c...

2,619 3
@lilydesignsystem/vue-locale-picker

Lily Design System Vue 3 locale picker: an icon button opening an APG listbox th...

1,260 3
@lilydesignsystem/vue-theme-picker

Lily Design System Vue 3 theme picker: an icon button opening an APG listbox tha...

1,137 3

Version History

Version Released Vue License
0.8.0 ^3.0.0 MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause
0.7.0 ^3.0.0 MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause
0.6.0 ^3.0.0 MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause
0.5.0 ^3.0.0 MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause
0.4.0 ^3.0.0 MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause