@fdecourt/vue-ganttastic

A flexible, accessible Gantt timeline engine and component library for Vue 3
1,724 1
Install
npm install @fdecourt/vue-ganttastic
Latest Version:3.0.8
Vue:3
License:MIT
Last Updated:Aug 27, 2026
Links: GitHub  ·  npm
Maintainer: fdecourt

Vue Ganttastic

Vue Ganttastic provides editable Vue 3 Gantt components plus a framework-independent, immutable timeline core.

Features

  • Automatic sub-lanes for overlapping bars, with stable visual positions during editing.
  • Mouse, touch and keyboard move/resize interactions.
  • Minute, hour, day, week or month scales and per-item snapping.
  • Exact elapsed-duration preservation when moving whole bars.
  • Duration bars, grouped bundles and point-in-time diamond or teardrop milestones.
  • Optional top or bottom timeline navigator with interval overview and accessible range zoom.
  • Cross-row moves, finish-to-start dependencies and collision policies.
  • Custom property mappings and functional adapters for arbitrary domain models.
  • Scoped slots for bars, row labels, tooltips and time-axis units.
  • IANA time zones, locale-aware labels and configurable week boundaries.
  • Locale-aware weekends, explicit closures and lazy country holidays at day precision.
  • A zoom-aware real-current-time line spanning the dynamic row area.
  • Responsive rendering, visible-range culling and a generic virtual-list component.
  • ESM, CommonJS, CSS and bundled TypeScript declarations.

Requirements

  • Node.js 20.19 or newer for package installation.
  • Vue 3.5.41 or newer in the consuming application.

Vue is a peer dependency and is never bundled with the library. Day.js is installed as the only runtime dependency.

Installation

Published package:

npm install @fdecourt/vue-ganttastic

Register the plugin once and import the stylesheet explicitly:

import { createApp } from "vue"
import App from "./App.vue"
import ganttastic from "@fdecourt/vue-ganttastic"
import "@fdecourt/vue-ganttastic/style.css"

createApp(App).use(ganttastic).mount("#app")

Components may also be imported individually:

import {
  GGanttChart,
  GGanttRow,
  GGanttVirtualList
} from "@fdecourt/vue-ganttastic"

Complete example

<script setup lang="ts">
import { ref } from "vue"
import type { GanttBarObject } from "@fdecourt/vue-ganttastic"

const analysis = ref<GanttBarObject[]>([
  {
    start: "2026-07-06 00:00",
    end: "2026-07-10 00:00",
    ganttBarConfig: {
      id: "analysis",
      label: "Analysis",
      hasHandles: true,
      snapTo: "day"
    }
  },
  {
    start: "2026-07-08 00:00",
    end: "2026-07-14 00:00",
    ganttBarConfig: {
      id: "research",
      label: "Research",
      snapTo: "day"
    }
  }
])

const delivery = ref<GanttBarObject[]>([
  {
    start: "2026-07-13 00:00",
    end: "2026-07-24 00:00",
    ganttBarConfig: {
      id: "implementation",
      label: "Implementation",
      hasHandles: true,
      dependsOn: "analysis",
      snapTo: "day"
    }
  },
  {
    start: "2026-07-31 16:30",
    ganttBarConfig: {
      id: "release",
      label: "Release",
      milestone: "diamond"
    }
  }
])
const selectedTaskId = ref<string | null>(null)

function addTask() {
  delivery.value.push({
    start: "2026-08-03 00:00",
    end: "2026-08-07 00:00",
    ganttBarConfig: {
      id: crypto.randomUUID(),
      label: "New task",
      hasHandles: true,
      snapTo: "day"
    }
  })
}

function removeTask(id: string) {
  analysis.value = analysis.value.filter(bar => bar.ganttBarConfig?.id !== id)
  delivery.value = delivery.value.filter(bar => bar.ganttBarConfig?.id !== id)
  selectedTaskId.value = null
}

function selectTask(bar: GanttBarObject) {
  selectedTaskId.value = bar.ganttBarConfig?.id ?? null
}
</script>

<template>
  <button type="button" @click="addTask">Add task</button>
  <button
    type="button"
    :disabled="!selectedTaskId"
    @click="selectedTaskId && removeTask(selectedTaskId)"
  >
    Remove selected task
  </button>

  <GGanttChart
    chart-start="2026-07-01 00:00"
    chart-end="2026-08-31 23:59"
    precision="week"
    zoom
    zoom-position="top"
    snap-to="day"
    time-zone="Europe/Paris"
    locale="en"
    overlap-layout="stack"
    allow-row-drag
    grid
    bar-start="start"
    bar-end="end"
    @click-bar="selectTask($event.bar)"
  >
    <GGanttRow row-id="analysis" label="Analysis" v-model:bars="analysis">
      <template #bar="{ config }">
        <span>{{ config.label }}</span>
      </template>
    </GGanttRow>

    <GGanttRow row-id="delivery" label="Delivery" v-model:bars="delivery" />
  </GGanttChart>
</template>

precision controls the displayed axis. snap-to controls interactions independently. A bar can override the chart with ganttBarConfig.snapTo and snapStep.

Timeline navigator and zoom

Enable zoom to display a compact navigator over the full chart-start / chart-end domain. Its overview is the union of every bar and milestone interval: activity is continuous where events touch or overlap and split into segments where the timeline is empty.

<GGanttChart
  chart-start="2026-01-01 00:00"
  chart-end="2026-12-31 23:59"
  precision="month"
  zoom
  zoom-position="bottom"
  @zoom-change="visibleRange = $event"
  @zoom-reset="visibleRange = $event"
>
  <!-- rows -->
</GGanttChart>

Drag either handle to resize the visible range or drag the selected window to move it. The native range handles support keyboard and touch input. Double-click the selection or use Reset to restore the full domain. zoom-position accepts top (default) or bottom.

The axis precision adapts to the visible duration and available width from month down to minute. Pass :zoom-auto-precision="false" to keep the configured precision. Both zoom events expose { start, end, originalEvent }; start and end follow date-format and are native Date values when :date-format="false".

Lower day and week units display only their number by default because the month is already shown above. Add timeaxis-unit-include-month to repeat the localized short month when desired. Month names follow locale; import the corresponding Day.js locale once before selecting it:

import "dayjs/locale/fr.js"
<GGanttChart locale="fr" timeaxis-unit-include-month ... />

Calendar and visual labels

Weekend columns receive a subtle full-height background at day precision. This follows the regional weekend returned for locale by the browser and is enabled by default; disable it with :highlight-weekends="false".

Add project closures through non-working-dates, or set holiday-country to an ISO 3166-1 alpha-2 code such as FR. National public holidays are loaded through date-holidays only when the country option is used. Both sources reuse the daily background layer.

<GGanttChart
  holiday-country="FR"
  :non-working-dates="['2026-05-15']"
  ...
/>

Enable bar-label-overflow when complete bar labels may extend beyond short bars. Their actual rendered size is then measured and bars and milestones share collision-free sub-lines, including with overlap-layout="overlay". Use hide-label-column-title to preserve the label column while leaving its top cell blank and transparent.

<GGanttChart
  locale="fr"
  bar-label-overflow
  hide-label-column-title
  ...
/>

Current-time line

The user's real current instant is shown by default as a continuous red 3-pixel line. It begins below the time axis, spans the complete dynamic row height and follows navigator zoom. It is hidden outside the visible range.

<GGanttChart current-time-color="#dc2626" :current-time-width="2" ... />

Set :current-time="false" to disable it. current-time-label and current-time-refresh remain available.

Data model

By default, bar-start, bar-end and bar-config-key select properties on each item. Metadata must contain a chart-wide unique string id:

interface Task {
  start: string | Date
  end?: string | Date // omitted for milestones
  ganttBarConfig: {
    id: string
    label?: string
    hasHandles?: boolean
    immobile?: boolean
    bundle?: string
    milestone?: "diamond" | "teardrop"
    dependsOn?: string | string[]
    snapTo?: "minute" | "hour" | "day" | "week" | "month" | false
    snapStep?: number
    style?: Record<string, string | number>
  }
}

For nested or domain-specific records, pass item-accessor with getId, getStart, getEnd, getConfig, setStart and setEnd. Setters are required only for interactive editing.

Interaction rules

  • overlap-layout="stack" gives overlapping bars stable independent sub-lanes.
  • push-on-overlap moves external obstacles; no-overlap rejects new collisions.
  • bundle moves related bars atomically while preserving offsets and durations.
  • milestone renders the item's start as a precise point; end is not required.
  • With bar-label-overflow, bar and milestone content is measured at its rendered screen size and assigned to the lowest free sub-line; equal-time milestones stack in input order.
  • dependsOn or chart-level connections moves finish-to-start successors transitively.
  • allow-row-drag requires a stable row-id and v-model:bars on every source/target row.
  • Whole-bar moves preserve exact elapsed milliseconds; resizing intentionally changes duration.
  • Native Date values require :date-format="false".

Custom rendering

Scoped slots replace content without replacing positioning or interaction behavior:

Component Slot Scope
GGanttRow bar { bar, config, lane, rowId }
GGanttRow milestone { bar, config, shape, lane, rowId }
GGanttRow row-label { label, rowId, bars, laneCount }
GGanttChart tooltip { bar, start, end }
GGanttChart row-label { label, rowId, bars, laneCount, index }
GGanttChart upper-timeunit, timeunit { label, value, date }

Historical slot aliases remain available for migration.

State management

The default update-mode="mutable" preserves direct interactive updates. With update-mode="controlled", the chart restores its temporary preview and emits typed change patches for Pinia, Vuex, event sourcing or undo/redo. Interactive controlled objects must remain writable during the preview; use the headless engine for frozen inputs.

Accessibility and performance

Bars expose keyboard controls, visible focus, ARIA descriptions and live announcements. Arrow keys move bars, Shift+Arrow resizes, Alt+Up/Down transfers rows, Enter commits and Escape restores. Visually hidden keyboard help and its live region are anchored inside the positioned chart, so they cannot extend the page outside a scrolling container. Reduced-motion preferences are respected.

Lane assignment is O(n log n) and collision queries use an interval index. Bars outside the chart range are culled. Use optimize-rendering for browser containment or GGanttVirtualList<T> for large independent resource lists.

Headless core

The core entry has no Vue or DOM dependency and never mutates its inputs:

import {
  proposeMove,
  createStepSnapPolicy,
  rejectStationaryOverlaps
} from "@fdecourt/vue-ganttastic/core"

It accepts a generic TimelineAdapter<T> and optional snap, dependency and collision policies.

License

Vue Ganttastic is available under the MIT License.

Related Packages

@infectoone/vue-ganttastic

A simple and customizable Gantt chart component for Vue.js

303,460 718
hy-vue-gantt

Evolution of vue-ganttastic package

39,308 42
vue-ganttastic

A simple and customizable Gantt chart component for Vue.js

35,238 718
@adesin-fr/vue-ganttastic

A simple and customizable Gantt chart component for Vue.js

1,061 0
@wirecube/vue-ganttastic

A simple and customizable Gantt chart component for Vue.js

338 0

Version History

Version Released Vue License
3.0.8 ^3.5.41 MIT
3.0.7 ^3.5.41 MIT
3.0.6 ^3.5.41 MIT
3.0.5 ^3.5.41 MIT
3.0.4 ^3.5.41 MIT