gantt-editor
v2.8.0
Published
Highly flexible, performant, framework-agnostic Gantt chart editor component for building applications that require resource allocation or task scheduling.
Readme
Gantt Editor
Highly flexible, performant, framework-agnostic Gantt chart editor component for building applications that require resource allocation or task scheduling.

Quick Start By Framework
npm install gantt-editor<script setup lang="ts">
import { ref } from "vue";
import GanttEditor, {
type GanttEditorDestination,
type GanttEditorDestinationGroup,
type GanttEditorSlot,
} from "gantt-editor/vue";
const startTime = ref(new Date("2025-01-01T00:00:00Z"));
const endTime = ref(new Date("2025-01-02T00:00:00Z"));
const slots = ref<GanttEditorSlot[]>([
{
id: "LH123-20250101-F",
displayName: "LH123 | F",
group: "LH123",
openTime: new Date("2025-01-01T10:00:00Z"),
closeTime: new Date("2025-01-01T12:00:00Z"),
destinationId: "chute-1",
hoverData: "<strong>LH123</strong><br><em>Gate opens 10:00</em>",
deadlines: [
{ id: "std", timestamp: new Date("2025-01-01T13:00:00Z").getTime(), color: "#9e9e9e" },
{ id: "etd", timestamp: new Date("2025-01-01T13:25:00Z").getTime(), color: "#1f1f1f" },
],
color: "#3498db",
},
]);
const destinations = ref<GanttEditorDestination[]>([
{ id: "chute-1", displayName: "Chute 1", active: true, groupId: "allocated" },
{ id: "UNALLOCATED", displayName: "Unallocated", active: true, groupId: "unallocated" },
]);
const destinationGroups = ref<GanttEditorDestinationGroup[]>([
{ id: "allocated", displayName: "Allocated Chutes", heightPortion: 0.8 },
{ id: "unallocated", displayName: "Unallocated Chute", heightPortion: 0.2 },
]);
</script>
<template>
<div style="height: 100vh; width: 100%;">
<GanttEditor
:isReadOnly="false"
:startTime="startTime"
:endTime="endTime"
:slots="slots"
:destinations="destinations"
:destinationGroups="destinationGroups"
:markedRegion="null"
:suggestions="[]"
@onChangeDestinationId="(slotId, destinationId, preview) => console.log(slotId, destinationId, preview)"
@onMoveSlotOnTimeAxis="(slotId, timeDiffMs, preview) => console.log(slotId, timeDiffMs, preview)"
@onSelectionChange="(slotIds) => console.log(slotIds)"
/>
</div>
</template>import { useMemo, useState } from "react";
import {
GanttEditor,
type GanttEditorDestination,
type GanttEditorDestinationGroup,
type GanttEditorSlot,
} from "gantt-editor/react";
export function App() {
const [startTime] = useState(() => new Date("2025-01-01T00:00:00Z"));
const [endTime] = useState(() => new Date("2025-01-02T00:00:00Z"));
const slots = useMemo<GanttEditorSlot[]>(
() => [
{
id: "LH123-20250101-F",
displayName: "LH123 | F",
group: "LH123",
openTime: new Date("2025-01-01T10:00:00Z"),
closeTime: new Date("2025-01-01T12:00:00Z"),
destinationId: "chute-1",
hoverData: "<strong>LH123</strong><br><em>Gate opens 10:00</em>",
},
],
[],
);
const destinations = useMemo<GanttEditorDestination[]>(
() => [{ id: "chute-1", displayName: "Chute 1", active: true, groupId: "allocated" }],
[],
);
const destinationGroups = useMemo<GanttEditorDestinationGroup[]>(
() => [{ id: "allocated", displayName: "Allocated Chutes", heightPortion: 1 }],
[],
);
return (
<div style={{ height: "100vh", width: "100%" }}>
<GanttEditor
isReadOnly={false}
startTime={startTime}
endTime={endTime}
slots={slots}
destinations={destinations}
destinationGroups={destinationGroups}
markedRegion={null}
suggestions={[]}
onChangeDestinationId={(slotId, destinationId, preview) => console.log(slotId, destinationId, preview)}
onMoveSlotOnTimeAxis={(slotId, timeDiffMs, preview) => console.log(slotId, timeDiffMs, preview)}
onSelectionChange={(slotIds) => console.log(slotIds)}
/>
</div>
);
}import { Component } from "@angular/core";
import {
GanttEditor,
type GanttEditorDestination,
type GanttEditorDestinationGroup,
type GanttEditorSlot,
} from "gantt-editor/angular";
@Component({
selector: "app-root",
standalone: true,
imports: [GanttEditor],
template: `
<div style="height: 100vh; width: 100%;">
<gantt-editor
[isReadOnly]="false"
[startTime]="startTime"
[endTime]="endTime"
[slots]="slots"
[destinations]="destinations"
[destinationGroups]="destinationGroups"
[markedRegion]="null"
[suggestions]="[]"
(onChangeDestinationId)="onChangeDestinationId($event)"
(onMoveSlotOnTimeAxis)="onMoveSlotOnTimeAxis($event)"
(onSelectionChange)="onSelectionChange($event)"
/>
</div>
`,
})
export class AppComponent {
startTime = new Date("2025-01-01T00:00:00Z");
endTime = new Date("2025-01-02T00:00:00Z");
slots: GanttEditorSlot[] = [
{
id: "LH123-20250101-F",
displayName: "LH123 | F",
group: "LH123",
openTime: new Date("2025-01-01T10:00:00Z"),
closeTime: new Date("2025-01-01T12:00:00Z"),
destinationId: "chute-1",
hoverData: "<strong>LH123</strong><br><em>Gate opens 10:00</em>",
},
];
destinations: GanttEditorDestination[] = [
{ id: "chute-1", displayName: "Chute 1", active: true, groupId: "allocated" },
];
destinationGroups: GanttEditorDestinationGroup[] = [
{ id: "allocated", displayName: "Allocated Chutes", heightPortion: 1 },
];
onChangeDestinationId([slotId, destinationId, preview]: [string, string, boolean]) {
console.log(slotId, destinationId, preview);
}
onMoveSlotOnTimeAxis([slotId, timeDiffMs, preview]: [string, number, boolean]) {
console.log(slotId, timeDiffMs, preview);
}
onSelectionChange(slotIds: string[]) {
console.log(slotIds);
}
}Angular note: multi-value outputs are emitted as tuples in the same order as the Vue/React callback arguments.
Shared API
All wrappers expose the same core model and behavior.
Required Inputs
startTime: DateendTime: Dateslots: GanttEditorSlotWithUiAttributes[]destinations: GanttEditorDestination[]destinationGroups: GanttEditorDestinationGroup[]suggestions: GanttEditorSuggestion[]markedRegion: GanttEditorMarkedRegion | nullisReadOnly: boolean
GanttEditorSlot supports generic slot deadlines:
deadlines?: Array<{ id: string; timestamp: number; color: string }>hoverData?: string(tooltip supports plain text and a limited HTML subset:<strong>,<em>,<br>)labelColor?: string(CSS color for slot text inside the bar)customOverlay?: ({ ctx, width, height, slot }) => void(optional custom canvas painter with slot-local coordinates where top-left is(0,0);ctxis uniformly scaled by slot height so overlay dimensions resize with the slot while preserving aspect ratio)
Common Optional Inputs
activateRulers: "ROW" | "GLOBAL" | nullverticalMarkers: GanttEditorVerticalMarker[]contextMenuActions: GanttEditorCanvasContextMenuAction[]slotContextMenuActions: GanttEditorSlotContextMenuAction[]topContentPortion: numberxAxisOptions: GanttEditorXAxisOptionshelpOverlayTiles: HelpOverlayTileDefinition[]helpOverlayTileIds: string[]features: GanttEditorFeature[]
Key Events
- Time range:
onChangeStartAndEndTime(start, end) - Destination move/copy (single and bulk):
onChangeDestinationId,onBulkChangeDestinationId,onCopyToDestinationId,onBulkCopyToDestinationId - Time-axis move/copy (single and bulk):
onMoveSlotOnTimeAxis,onBulkMoveSlotsOnTimeAxis,onCopySlotOnTimeAxis,onBulkCopySlotsOnTimeAxis - Resize:
onChangeSlotTime(slotId, openTime, closeTime) - Selection and click interactions:
onSelectionChange,onClickOnSlot,onHoverOnSlot,onDoubleClickOnSlot,onContextClickOnSlot - Vertical markers:
onChangeVerticalMarker,onClickVerticalMarker - Canvas context menu action:
onContextMenuAction(actionId, timestamp, destinationId) - Slot context menu action:
onSlotContextMenuAction(actionId, slotId)
Feature Flags
features is an allow-list. Omit it to keep all interactions enabled.
Supported ids:
select-slotsbrush-select-slotsresize-slot-timeapply-slot-suggestionscollapse-topicscanvas-context-menumove-vertical-markersmove-vertical-markers-from-context-menumove-slots-to-destinationbulk-move-slots-to-destinationcopy-slots-to-destinationbulk-copy-slots-to-destinationmove-slots-on-time-axisbulk-move-slots-on-time-axiscopy-slots-on-time-axisbulk-copy-slots-on-time-axispreview-slots-to-destinationpreview-slots-on-time-axiscopy-modifier-alttime-axis-modifier-shift
Exposed Methods
clearSelection()
Local Development
- Install:
npm install - Start demos:
npm run dev:vuenpm run dev:reactnpm run dev:angular
- Default dev command:
npm run dev(Vue demo)
