Gantt planning and dependencies
Configure calendars, task hierarchy and cross-table scheduling with atomic previews in React and Vue.
Gantt is an optional display mode shared by the React and Vue registries. Map two date columns and the table derives the schedule from the rows it already lists, the way Kanban and Gallery need only their own column mappings. Supply a transactional adapter instead when you need atomic commits, relationships or server-side paging. Either way the table provides the timeline, the relationship editor and the impact preview.
View settings
Open View → Gantt settings for zoom, the first day of the week and dependency visibility. These controls use the same settings panel as other display modes; previous/next/today and reload remain beside the timeline. On mobile, settings choices stay inside one bottom drawer. Task and dependency dialogs also open as bottom sheets with bounded height. Only overflowing content scrolls; timeline panning remains available. Existing saved-view and URL fields are unchanged. Every timeline, dialog and settings label reads views.gantt.* from the table translations; each key you leave out keeps its built-in English or French wording.
Try the Gantt
Choose React or Vue below: each tab runs that framework with the same tasks, calendars and cross-table dependencies. Preview and Code work like the other examples. Expand opens a full-size example; Reset restores only this preview.
Move Build the experience one day forward, or focus its bar and press the right arrow.
Review the changes to the parent, acceptance review and release in the other table. Cancel to keep the initial dates, or apply the complete preview.
Open Default view and switch to Table, Kanban or Gallery to inspect the same records. Click a task to edit its dates, parent and dependencies.
The two entrypoints use the same sample graph, calendars, list action and saved views. The in-memory adapter demonstrates the transaction contract; a production application supplies durable storage.
Sample data. Changes stay in this preview.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useMemo, useState } from "react";
import { Toaster } from "sonner";
import { DataTable } from "@/components/ui/yayaw-table/components/data-table";
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { createMemoryPlanningAdapter } from "@/components/ui/yayaw-table/planning/adapter";
import {
createGanttDemoViews,
demoGanttConfig,
demoPlanningConfig,
demoPlanningSnapshot,
ganttDemoColumns,
ganttDemoForm,
listPlanningDemo,
} from "../shared/gantt-data";
import type { ExamplePresentation } from "./presentation";
const config = defineTableConfig({
id: "gantt-demo",
columns: {
definitions: ganttDemoColumns,
visible: ["name", "start", "end", "status"],
order: ["name", "start", "end", "status"],
mandatory: ["name"],
},
table: {
enableViews: true,
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
planning: demoPlanningConfig,
gantt: demoGanttConfig,
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowCreate: false,
allowDelete: false,
allowDuplicate: false,
allowEdit: true,
allowInlineEdit: true,
inlineEdit: { enabled: true },
rowClickMode: "default",
defaultPageSize: 20,
},
translations: {
namespace: "gantt-demo",
keys: {
title: "Autumn launch",
description: "Tasks and releases share one planning graph.",
},
},
});
export default function GanttExample(presentation: ExamplePresentation = {}) {
const [queryClient] = useState(() => new QueryClient());
const integration = useMemo(() => {
const adapter = createMemoryPlanningAdapter({
snapshot: demoPlanningSnapshot(),
config: demoPlanningConfig,
});
const actions = {
planning: adapter.actions,
views: createGanttDemoViews(),
list: (params: Record<string, unknown>) =>
Promise.resolve(listPlanningDemo(adapter.getSnapshot(), params)),
};
return { getTableConfig: () => config, getTableActions: () => actions };
}, []);
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
getFormConfig={() => ganttDemoForm}
tableType="gantt-demo"
{...integration}
details={{ presentation: "drawer", title: (row) => String(row.name) }}
/>
<Toaster />
</NuqsAdapter>
</QueryClientProvider>
);
}Sample data. Changes stay in this preview.
<script setup lang="ts">
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
import { DataTable, defineTableConfig } from "@/components/ui/yayaw-table-vue";
import { createMemoryPlanningAdapter } from "@/components/ui/yayaw-table-vue/planning/adapter";
import type { TableListParams } from "@/components/ui/yayaw-table-vue/types";
import {
createGanttDemoViews,
demoGanttConfig,
demoPlanningConfig,
demoPlanningSnapshot,
ganttDemoColumns,
ganttDemoForm,
listPlanningDemo,
} from "../shared/gantt-data";
const config = defineTableConfig({
id: "gantt-demo",
columns: {
definitions: ganttDemoColumns,
visible: ["name", "start", "end", "status"],
order: ["name", "start", "end", "status"],
mandatory: ["name"],
},
table: {
enableViews: true,
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
planning: demoPlanningConfig,
gantt: demoGanttConfig,
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowCreate: false,
allowDelete: false,
allowDuplicate: false,
allowEdit: true,
allowInlineEdit: true,
inlineEdit: { enabled: true },
rowClickMode: "default",
defaultPageSize: 20,
},
translations: {
namespace: "gantt-demo",
keys: {
title: "Autumn launch",
description: "Tasks and releases share one planning graph.",
},
},
});
const adapter = createMemoryPlanningAdapter({
snapshot: demoPlanningSnapshot(),
config: demoPlanningConfig,
});
const actions = {
views: createGanttDemoViews(),
planning: adapter.actions,
list: async (params: TableListParams) =>
listPlanningDemo(adapter.getSnapshot(), { ...params }),
};
</script>
<template>
<DataTable v-bind="$props"
table-type="gantt-demo"
:config="config"
:get-form-config="() => ganttDemoForm"
:get-table-actions="() => actions"
:details="{ presentation: 'drawer', title: row => String(row.name) }"
/>
</template>Start from the table's own rows
A table that maps gantt.startColumn and gantt.endColumn needs no planning adapter: the graph is
derived from its existing list action, and moving a bar saves through its existing update action.
The example below is the catalog from the quick start with two date columns mapped.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useMemo, useRef, useState } from "react";
import { DataTable, defineTableConfig } from "@/components/ui/yayaw-table";
import { products } from "../shared/products";
import { listRecords } from "./list-records";
import type { ExamplePresentation } from "./presentation";
/**
* A Gantt over the catalog from the quick start: no planning adapter, no graph.
* Mapping two date columns is the whole configuration.
*/
const config = defineTableConfig({
id: "restock-plan",
columns: {
definitions: [
{ id: "name", header: "Product", type: "text" },
{ id: "restockFrom", header: "From", type: "date" },
{ id: "restockTo", header: "To", type: "date" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["select", "name", "restockFrom", "restockTo", "status"],
visible: ["name", "restockFrom", "restockTo", "status"],
mandatory: ["name"],
},
table: {
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowEdit: true,
enableRowSelection: true,
planning: {
enabled: true,
scopeId: "docs/restock-plan",
sourceId: "products",
},
gantt: {
titleColumn: "name",
startColumn: "restockFrom",
endColumn: "restockTo",
},
defaultPageSize: 10,
},
translations: {
namespace: "restock-plan",
keys: {
title: "Restock plan",
description: "The catalog table with two date columns mapped.",
},
},
});
export default function GanttRowsExample(
presentation: ExamplePresentation = {}
) {
const [queryClient] = useState(() => new QueryClient());
// A stable store stands in for the application's own records.
const store = useRef(products.map((product) => ({ ...product })));
const integration = useMemo(
() => ({
getTableConfig: () => config,
getTableActions: () => ({
list: (params: Record<string, unknown>) =>
listRecords(store.current, params),
update: (id: string, patch: Record<string, unknown>) => {
store.current = store.current.map((row) =>
row.id === id ? { ...row, ...patch } : row
);
return Promise.resolve({ success: true });
},
}),
}),
[]
);
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
getRowId={(row) => String(row.id)}
tableType="restock-plan"
{...integration}
/>
</NuqsAdapter>
</QueryClientProvider>
);
}<script setup lang="ts">
import { ref } from "vue";
import { DataTable, defineTableConfig } from "@/components/ui/yayaw-table-vue";
import type {
TableListParams,
TableRecord,
} from "@/components/ui/yayaw-table-vue/types";
import { products } from "../shared/products";
import { listRecords } from "./list-records";
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
/**
* A Gantt over the catalog from the quick start: no planning adapter, no graph.
* Mapping two date columns is the whole configuration.
*/
const config = defineTableConfig({
id: "restock-plan",
columns: {
definitions: [
{ id: "name", header: "Product", type: "text" },
{ id: "restockFrom", header: "From", type: "date" },
{ id: "restockTo", header: "To", type: "date" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["select", "name", "restockFrom", "restockTo", "status"],
visible: ["name", "restockFrom", "restockTo", "status"],
mandatory: ["name"],
},
table: {
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowEdit: true,
enableRowSelection: true,
planning: {
enabled: true,
scopeId: "docs/restock-plan",
sourceId: "products",
},
gantt: {
titleColumn: "name",
startColumn: "restockFrom",
endColumn: "restockTo",
},
defaultPageSize: 10,
},
translations: {
namespace: "restock-plan",
keys: {
title: "Restock plan",
description: "The catalog table with two date columns mapped.",
},
},
});
// A stable store stands in for the application's own records.
const store = ref<TableRecord[]>(products.map((product) => ({ ...product })));
const actions = {
list: (params: TableListParams) => listRecords(store.value, { ...params }),
update: (id: string, patch: TableRecord) => {
store.value = store.value.map((row) =>
row.id === id ? { ...row, ...patch } : row
);
return Promise.resolve({ success: true });
},
};
</script>
<template>
<DataTable v-bind="$props"
table-type="restock-plan"
:config="config"
:get-row-id="(row: TableRecord) => String(row.id)"
:get-table-actions="() => actions"
/>
</template>A derived planning is not transactional: each affected record is patched through update rather than
committed together, so a partial failure is reported and the graph reloaded to show what was stored.
Dependency editing stays off, because rows carry dates and hierarchy but no relationships. Add
gantt.parentColumn for a hierarchy from a scalar parent column, and gantt.calendarColumn to pick a
calendar per row. createRowsPlanningAdapter builds the same adapter explicitly when you want to pass
calendars, dependencies or a different page size.
The timeline shows the whole graph while the table holds the rows its list returned, so a record
outside the current page appears with its planning label, without table cells or a selection box.
Enable planning
Add "gantt" to table.displayModes and explicitly enable table.planning. A visual table instance is not a business identity: use a stable application source and a scope that includes the organization/project boundary.
const gantt = {
titleColumn: "name",
startColumn: "start",
endColumn: "end",
zoom: "week" as const,
weekStartsOn: 1,
showDependencies: true,
};
const planning = {
enabled: true,
scopeId: "organization/project",
sourceId: "tasks",
scheduling: "preview" as const,
};
// Add these properties to the table configuration from the quick start.
const table = {
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
allowEdit: true,
planning,
gantt,
};Use the React or Vue setup. Both editions expose the same planning types, calculatePlanning, resolvedPlanningSnapshot, planningTasksFromRows, and createMemoryPlanningAdapter. The engine has no framework dependency.
Normalize records
A task has a composite ref: {source, id}, a label, civil start/end dates, an optional parent reference and an optional calendar. Hierarchy and scheduling dependencies are distinct relations. Identical record IDs in different sources remain different tasks.
planningTasksFromRows({source, rows, getId, gantt}) reads the configured columns and normalizes existing subRows. It returns {source, tasks}: include that returned source in the snapshot because its fields map is also used for inline/form record patches. Custom getChildren, getParent and toDate adapters support other application schemas. Pass the same gantt field mappings to the normalizer and table configuration.
Applications can construct normalized tasks directly instead. source.fields maps title, start, end, scalar parent ID and calendar fields without imposing database names. Store cross-source parents as composite references separately from scalar parent columns.
Dates are YYYY-MM-DD civil dates, with inclusive ends. An unscheduled task has both dates set to null; it stays in the tree. A dependency that needs unavailable dates cannot be recalculated. Convert timestamps explicitly using the application's time zone.
Configurable rules
| Setting | Default |
|---|---|
planning.scheduling | preview |
planning.parentDates | rollup |
planning.hierarchy | true |
planning.allowDateEdit | true |
planning.allowDependencyEdit | true |
planning.allowHierarchyEdit | true |
planning.allowCrossTableDependencies | true |
planning.allowSummaryMove | true |
planning.dependencyTypes | FS, SS, FF, SF |
planning.maxCalendarSearchDays | 36600 |
gantt.zoom | week |
gantt.weekStartsOn | 1 (Monday) |
gantt.showDependencies | true |
gantt.parentColumn | unset; no hierarchy from the rows |
gantt.calendarColumn | unset; the source or planning calendar applies |
gantt.height | 480 pixels |
The three editing flags restrict the existing permissions. They cannot grant a right denied by allowEdit, canEditRow, task authorization or the server.
preview requires confirmation. manual reports violated constraints while preserving successor dates. automatic uses the same validation and atomic apply contract, then commits without a confirmation click. parentDates: "independent" gives parents their own dates instead of calculating descendant bounds.
Calendars and dependency types
Calendars contain working weekdays (0 is Sunday) and date exceptions that open or close individual days. Resolution is task calendar, then source calendar, then the planning's default calendar. Changing the displayed first weekday never changes task dates.
FS links a predecessor finish to successor start; SS links starts; FF links finishes; SF links predecessor start to successor finish. Ends are inclusive in records and exclusive at constraint boundaries. For example, FS with a Friday finish and zero offset starts Monday in a Monday–Friday calendar. An offset of one working day starts Tuesday.
Offsets may be positive or negative. They count successor working days by default; lagUnit: "calendarDays" counts civil days. Scheduling moves successors only when needed, preserving existing margins. Summary movement shifts descendants while preserving each leaf's working duration; different calendars can change elapsed gaps. Resize children to change a summary duration.
Self-links, cycles, dependencies between a summary and its descendants, and indirect cycles through multiple groups or sources are rejected.
Load, preview and apply
Provide actions.planning alongside the table actions:
load({scopeId, sourceId, cursor?, signal?})resolves all required tasks, ancestors, links, sources and calendars, beyond table filters and pagination. Graph pages must share a revision. Only a complete graph can be scheduled.preview({scopeId, sourceId, revision, mutations})validates permissions and calculates every proposed change without writing. Return{success: true, data: {id, revision, changes, dependencies, warnings}}; retain the proposal behind its opaque ID.apply({scopeId, sourceId, previewId, revision, idempotencyKey})rechecks the proposal, permissions and revision, then saves every record and relationship in one transaction. Return the complete updated snapshot only after the whole commit succeeds.
A stale proposal returns code: "stale-preview" and requires reloading and calculating a new preview. Other failures keep the preview available. Retrying an uncertain network response uses the same idempotency key. The server must bind proposals and durable idempotency results to the authenticated application context. Never accept arbitrary client-supplied changes or present partial writes as success.
The executable memory adapter implements this contract for examples, including commit-time validation and concurrent revision checks. Production applications supply durable transactions, authoritative permissions and any additional business validation. Create/delete/custom mutations remain application-owned and must maintain graph integrity and update its revision.
Editing and saved views
The timeline uses a compact period toolbar, a responsive task list, subtle grid lines, and task and summary bars drawn from the host's own theme tokens, so colors encode neither task status nor scheduling rules and follow light and dark mode. Today's date has a header marker and a vertical guide. Resize handles appear on hover or keyboard focus and remain visible on touch devices.
The Gantt shows a collapsible tree, calendar shading, bars and links. Its left column carries the table's own selection and title cells, so selecting there feeds bulk actions and clicking a title opens the record detail, exactly as in Table, Kanban and Gallery. Click a bar to edit dates, its parent and dependencies. Choose the predecessor source, record, type and signed offset with labelled keyboard-accessible controls. The common record detail header also exposes Planning from every display mode.
Drag a bar to move a task, or either handle to resize it. Left/Right on a focused bar or handle moves one day; Shift+Left/Right moves seven. Inline edits, catalogue forms and bulk record patches pass through the same planning preview and apply actions.
The preview includes old/new dates, hierarchy, record changes and link changes, including affected records outside the current view. Cancelling performs no apply call. Successful commits invalidate every mounted instance in the same planning scope; open previews retain their revision checks.
Saved views and scoped URLs contain zoom, weekStartsOn, showDependencies and anchorDate. Calendars, field mappings and planning rules remain application data/configuration. Filtering changes display only; it retains the full constraint graph. Sorting preserves hierarchy while ordering siblings.
The timeline virtualizes rows and day columns inside a navigable 180-day window. Previous/next/today and zoom controls navigate longer schedules. Paths are drawn when both endpoints are rendered; the relationship editor retains links to hidden endpoints.
Runnable examples
In the Table repository, run bun run gantt:dev for React or bun run vue:dev and open ?example=gantt for Vue. React's ?example=rows runs the adapter-free variant above. Both examples share tasks, a holiday calendar, an unscheduled record and a release in a second source with a colliding record ID. They include saved views and a catalogue form. Their stores reset on reload.
Open the integrated React example or Vue example directly. Sample changes stay in your browser session.
See the repository's engine contract and production adapter contract for implementation details and shared regression coverage.