Trang này chưa có bản tiếng Việt. Nội dung dưới đây là bản tiếng Anh.
Composites
Gantt
Tasks on a calendar: names on the left, bars by date on the right, arrows for what waits on what. One component covers both ends of planning in Comitor Tasks — the sprint, by day and week, with a critical path and a holiday to plan around, and the roadmap, by month, quarter and year, with projects and milestones. The dates are yours; the Gantt draws them, lets people drag them, and tells you what they changed.
- Import
import { Gantt, GANTT_SCALES, type GanttItem } from '@comitor/ui'— the main entry, with no optional peer. Its rows are virtualised with@tanstack/react-virtual, already a dependency of the package.- Client
Ganttstarts with"use client", and a Gantt that moves needs state and anonItemChange, so it lives in a client component of yours.GANTT_SCALESand the types come from a plain module, so a Server Component can read the array too. On the server there is no frame to measure yet: the HTML holds the header and no rows, as withDataGrid.- Origin
- Built in the package for
3.12.0— Dice UI has no Gantt, and the two candidates read to the code fell short: Kibo UI’s has no dependency arrows, no virtualisation and no keyboard, and pulls in three libraries; SVAR’s keeps the data in a store of its own, themes itself outside the tokens and keeps the critical path for its paid edition. The roadmap half borrows the feature set of Linear’s Timeline — projects, links between them, milestones, day-to-year zoom. Linear draws no task-level Gantt, so that half follows the common conventions. See the changelog.
The contract
Six rules. Everything else on this page follows from them, and most of the surprises come from forgetting one.
- Dates are strings.
yyyy-MM-dd, never aDate, andendis the last day worked, not the day after: a one-day task hasstart === end. Inside, each date is a whole number of days since 1970-01-01 — no time, no time zone, no 23-hour day when the clocks change. A deadline of 13 November is the same column in Hà Nội and in San Francisco. - Controlled.
itemsanddependenciesgo in;onItemChangeandonDependencyCreatecome out. While a bar is being dragged it follows a draft inside the component, and you hear about it once, on drop — not sixty times a second. Each arrow-key press is one call; batch them yourself if each one is a request. - A flat list, in display order. No sorting, no tree, no folding. You keep the tree, leave out the children of a folded row, and pass
depth,hasChildrenandexpandedso the left column can draw it. - The critical path is yours. It depends on working calendars, lags and each app’s rules, so the package does not compute it. Set
criticalon items and dependencies; the Gantt gives those bars a 2px--destructive-inkborder and those arrows a solid 2px red line. - A broken dependency is the package’s to find. Four relation types, checked on dates with both ends inclusive: finish-to-start wants the next task to start after the day the first one ends; start-to-start, not to start before it; finish-to-finish, not to end before it; start-to-finish, not to end before it starts. A broken one is a dashed red arrow — redrawn while you drag — and the later task gets the sentence “Starts before a task it depends on” for screen readers.
- Colour is a tone. Eleven names, each with a fill, a text colour on that fill and an ink for borders, so a bar follows dark mode and the high-contrast palette. No free colour: a hex code cannot be checked for contrast.
import { Gantt, type GanttDependency, type GanttItem } from '@comitor/ui'
const items: GanttItem[] = [
// One row per entry, in display order. depth only indents and sets aria-level.
{ id: 'otc', title: 'Sign in with a one-time code', type: 'summary', start: '2026-10-26', end: '2026-11-13',
hasChildren: true, expanded: true },
{ id: 'DS-406', title: 'Send the code by email', depth: 1, start: '2026-11-02', end: '2026-11-05',
tone: 'teal', progress: 60, critical: true }, // end is INCLUSIVE: four days
{ id: 'DS-388', title: 'Rate-limit the one-time codes', depth: 1, start: '2026-11-05', end: '2026-11-10',
tone: 'orange' },
{ id: 'DS-414', title: 'Load-test the code endpoint', depth: 1 }, // no dates: unscheduled, no bar
{ id: 'qa', title: 'Sign-in ready for QA', type: 'milestone', depth: 1, start: '2026-11-13' },
]
const dependencies: GanttDependency[] = [
// finish-to-start by default. DS-388 starts the day DS-406 ends: the package
// flags it — a dashed red arrow, and "Starts before a task it depends on".
{ id: 'l3', from: 'DS-406', to: 'DS-388' },
]
<Gantt className="h-[32rem]" items={items} dependencies={dependencies} onItemChange={save} />A sprint
Two groups, seven tasks, two milestones. DS-406 → DS-407 → QA → Release is marked as the critical path; DS-388 starts the day DS-406 ends, which breaks its finish-to-start link, so its arrow is dashed. DS-414 has no dates yet — hover its row and click to give it some. DS-405 is done and read-only. Wednesday 11 November is the team’s day off, in nonWorkingDays. Everything that comes out of the Gantt is printed under it: that line is the whole API surface you save from.
Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm.
Last call: Drag a bar, press an arrow key on a row, or link two bars…
- Drag a bar to move it, or one of its edges to resize it; the dates appear at both ends while you do, and the view scrolls when you reach its edge. Esc mid-drag puts the bar back.
- Link by hovering a bar and dragging the dot after its end onto another bar. A summary cannot be either end of a link, nor can a row with no dates.
- Click an arrow and
onDependencyClickfires — the demo only reports it. Double-click a bar, or click a name, foronItemOpen. - The critical path does not follow your drags. The page set those flags by hand; your app recomputes them after each save.
- A bar narrower than 56px writes its name beside it instead of inside. A bar scrolled out of view leaves a round chevron at the edge of the axis that scrolls back to it.
Folding is the app’s
The Gantt draws a chevron on any row with hasChildren and points it by expanded, and that is all. Clicking it — or Space — calls onExpandedChange; leaving the children out of items is your job. The same goes for summary bars: their dates are whatever you pass, so this demo spans each one over its children on every render. Drag the Tasks 2.0 ships diamond to the right and Launch and Comitor Tasks 2.0 stretch with it; drag it left of Beta’s end and its link breaks. Discovery starts folded, and its link into Gantt view is gone with it: a dependency whose end is not in items is skipped.
Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm.
items: 9 of 11 rows — tasks2, discover, build, gantt, wbs, assistant, launch, beta, ship
The records here have the same shape as Tree View’s — parentId plus array order, the way a SQL table with parent_id and position returns them — so one list can feed an outline on one side and the Gantt on the other. Reordering and re-parenting happen in the outline: a bar cannot be dragged to another row.
A roadmap
The same component at the far end of the zoom: projects instead of tasks, months and quarters instead of days, milestones where a project lands. range pins the axis to a fiscal year instead of padding it around the bars, and toolbar={false} swaps the built-in bar for the app’s own — three scale buttons built from GANTT_SCALES, because a roadmap has no use for days. The High-contrast audit is finished and read-only.
Roadmap · FY 2026–27
Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm.
What leaves with the built-in toolbar
Two things, and the package offers no handle to get them back. The Today button — there is no ref method to scroll to a date. The anchor when zooming — the built-in buttons keep the date in the middle of the view in place when the scale changes; a scale prop changed from outside keeps the scroll offset, which now points at another date. Here the fixed range keeps the axis short enough not to notice. If you need either, keep the bar and pass a node instead: toolbar={<Button>Reset</Button>} puts it at the right end of the built-in one, as the sprint above does.
Five scales
GANTT_SCALES lists them from near to far. Each one fixes how wide a day is, what the two header rows count, how far Shift + an arrow moves a bar, how much the axis is padded around the bars when there is no range, and how long a click makes an unscheduled task. At month and beyond a day is under 5px, so weekends and holidays are no longer shaded — they would only be noise.
| Scale | px / day | Top row | Bottom row | Shift + ←→ | Off days | Padding | Click to schedule |
|---|---|---|---|---|---|---|---|
day | 36 | month and year | weekday initial and day | 1 week | yes | 14 days | 1 day |
weekdefault | 14 | month and year | first day of the week | 1 week | yes | 42 days | 5 days |
month | 4.5 | year | month | 1 month | no | 120 days | 14 days |
quarter | 1.6 | year | quarter | 1 month | no | 365 days | 30 days |
year | 0.5 | — | year | 1 quarter | no | 730 days | 90 days |
On first render today stands a third of the way across the view — what is ahead usually matters more than what is behind. A bar is never narrower than 6px, so it can still be grabbed at year scale, where a day is half a pixel. Today is a gold chip in the header and a line down the rows; at day scale the chip is the day’s own number.
Tones and shapes
Every tone, read-only. A task bar uses the same soft tint as StatusPill and the letter avatar: --x at 15% over an opaque card fill, with its name and border in --x-ink. The border is what holds it apart from the page — the tint alone is too faint. primary takes the avatar's gold pair (gold-100 / gold-600) and neutral writes in --foreground. Marks with no text — the summary bar, the milestone diamond, the progress strip — use the -ink step alone, since they carry the information themselves; the default neutral tone draws them in --muted-foreground. red paints with --destructive, the one red meant to carry text. Switch the site to dark mode and every bar follows; the high-contrast palette (data-contrast="high") reaches them the same way.
Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm.
Keyboard
Everything a drag does to dates, a key does too. The arrow keys move dates only on an editable task or milestone — not on a summary, a read-only row, a row without dates, or a Gantt without onItemChange. On a Mac, Alt is ⌥.
| Keys | Result |
|---|---|
Tab | The whole grid is one tab stop: the selected row. Clicking a name or a cell puts focus on its row, not on the cell. |
↑ ↓ | Previous or next row — scrolled into view, and its bar scrolled in sideways. |
Home End PageUp PageDown | First row, last row, a screenful up or down. |
← → | Move the task or milestone one day. One press is one onItemChange with kind 'move', and one announcement. |
Shift+← Shift+→ | Move by the scale's big step — a week at day and week, a month at month and quarter, a quarter at year. |
Alt+← Alt+→ | Change the end date (kind 'resize-end'). It stops at the start date. A milestone ignores Alt and moves. |
Alt+Shift+← Alt+Shift+→ | Change the start date (kind 'resize-start'). It stops at the end date. |
Enter | onItemOpen for the row. |
Space | Expand or collapse a row with hasChildren — that is, call onExpandedChange. |
Esc | While dragging with the pointer: cancel. The bar goes back and nothing is reported. |
Two things have no key. Linking is a pointer gesture — and a mouse one at that, since the dot is hidden on touch screens — so an app needs another way to add a dependency: an Add dependency item in the task’s menu, which is why onDependencyCreate receives plain data. Removing one is the same story, since onDependencyClick lives on the arrow. Scheduling an unscheduled task is a click on its empty row; from the keyboard it takes a date field in the task’s detail view.
Language
The 24 strings of the gantt namespace follow the nearest ComitorLocaleProvider — English by default, Vietnamese built in (set the language once, at the root). So do the dates: every date in the header is written by Intl.DateTimeFormat in the provider’s locale. The task names stay as they are — they are the app’s data. The demo starts in Vietnamese: focus a row and press →, and the line under it prints what the live region says; then switch the language and do it again.
Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm.
Announced: …
On focus: Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm.
labelsisPartial<GanttLabels>, merged key by key: the component’s own prop wins, then the nearest provider, then English.- Of those, 8 are functions, because a date, a number or a name sits inside the sentence and word order differs by language. Dates arrive already formatted in the provider’s locale — the function only builds the sentence. Functions cannot cross from a Server Component into a client one, so set them in a client component.
- The provider chooses the strings; it does not set
lang. Keep<html lang>in step, as the demo does for its own region.
| Key | English | Tiếng Việt |
|---|---|---|
label | Timeline | Trục thời gian |
sidebarTitle | Name | Tên |
scaleLabel | Zoom | Thu phóng |
scaleDay … scaleYear | Days · Weeks · Months · Quarters · Years | Ngày · Tuần · Tháng · Quý · Năm |
today | Today | Hôm nay |
quarter(4, 2026) | Q4 | Q4 |
itemRange(start, end, 4) | Nov 2, 2026 – Nov 5, 2026, 4 days | 2 thg 11, 2026 – 5 thg 11, 2026, 4 ngày |
milestoneDate(date) | Milestone on Nov 5, 2026 | Mốc ngày 5 thg 11, 2026 |
progress(60) | 60% complete | Hoàn thành 60% |
unscheduled | No dates | Chưa có ngày |
critical | On the critical path | Nằm trên đường găng |
conflict | Starts before a task it depends on | Bắt đầu trước khi việc phụ thuộc xong |
instructions | Use Up and Down arrows to move between rows. Left and Right arrows move the dates by one day; hold Shift to move by a larger step. Alt with Left or Right changes the end date; Alt and Shift changes the start date. Enter opens the item, Space expands or collapses a group. | Dùng phím mũi tên lên, xuống để chuyển dòng. Mũi tên trái, phải dời ngày đi một ngày; giữ Shift để dời một bước lớn. Alt cùng mũi tên trái, phải đổi ngày kết thúc; Alt và Shift đổi ngày bắt đầu. Enter mở mục, Space mở hoặc thu một nhóm. |
rescheduled(title, range) | Send the code by email: Nov 2, 2026 – Nov 5, 2026 | Gửi mã qua email: 2 thg 11, 2026 – 5 thg 11, 2026 |
expand · collapse | Expand · Collapse | Mở rộng · Thu gọn |
scrollToItem(title) | Scroll to Release 1.4 | Cuộn tới Phát hành 1.4 |
linkHandle(title) | Drag to connect Send the code by email to the next task | Kéo để nối Gửi mã qua email với việc sau |
dependency(from, to) | DS-406 → DS-407 | DS-406 → DS-407 |
scheduleHint | Click to schedule | Bấm để xếp lịch |
Read live from comitorMessagesEn.gantt and comitorMessagesVi.gantt, with dates as each locale formats them.
Props
Exported beside the component: GANTT_SCALES and the types GanttItem, GanttDependency, GanttDependencyType, GanttItemChange, GanttDependencyDraft, GanttScale, GanttTone and GanttLabels.
Gantt
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
itemsbắt buộc | GanttItem[] | — | The rows, in display order. The Gantt does not sort them and does not hide the children of a folded row — filter the list yourself (see Folding). |
dependencies | GanttDependency[] | [] | The arrows. One whose from or to is not in items, or has no dates, is skipped silently — so folding a group also hides the arrows into it. |
onItemChange | (change: GanttItemChange) => void | — | Dates changed by a drag, a resize, an arrow key or a click on an unscheduled row. Once per drop, once per key press. The Gantt writes nothing: update your data and pass items again. Without it nothing can be edited. |
onDependencyCreate | (dependency: GanttDependencyDraft) => void | — | A dot at the end of each editable bar, dragged onto another bar. Passing it is what shows the dots. Mouse only (hidden on touch screens), always finish-to-start, and with no duplicate or cycle check. |
onDependencyClick | (dependency: GanttDependency) => void | — | A click on an arrow (12px hit area along the line) — to open a menu that removes it, say. Pointer only. |
onItemOpen | (item: GanttItem) => void | — | Enter on a row, a double click on a bar or milestone, or a click on the name in the left column. |
onExpandedChange | (id: string, expanded: boolean) => void | — | The chevron of a row with hasChildren, or Space on it. Without it the chevron still shows but does nothing. |
scale · defaultScale · onScaleChange | GanttScale | 'week' | The zoom, controlled or not. The built-in toolbar keeps the date in the middle of the view when it changes scale; changing scale from outside keeps the scroll offset instead. |
today | string | the user's clock | yyyy-MM-dd. Pass it from the server when the page is server-rendered: the today line and chip are drawn during the server render, and the server's date is not always the browser's. |
range | { start: string; end: string } | every bar + today, padded | A fixed span for the axis, snapped out to whole cells of the scale's bottom row. Without it the axis covers every bar and today, padded per scale (see Five scales). |
weekendDays | number[] | [0, 6] | Days of the week to shade, 0 = Sunday … 6 = Saturday. Shading only — durations are calendar days. |
nonWorkingDays | string[] | — | Holidays and days in lieu, yyyy-MM-dd, shaded like a weekend at day and week scale. |
weekStartsOn | 0 | 1 | 1 | 1 = Monday (ISO), 0 = Sunday. |
readOnly | boolean | false | Locks every edit — drag, resize, keys, linking, scheduling — and sets aria-readonly. item.readOnly locks one row. |
toolbar | boolean | ReactNode | true | The bar above: scale buttons and Today. false hides it; a node keeps it and puts the node at its right end. |
renderSidebarItem | (item: GanttItem) => ReactNode | — | The name cell's content in place of the bare title — a task key, an avatar. It sits in a truncating block: keep it inline. |
sidebarWidth · sidebarTitle | number · ReactNode | 280 · labels.sidebarTitle | The left column's width in px and its header. |
rowHeight | number | 36, or 44 on touch | Bars are 12px shorter than the row. The touch default comes from (pointer: coarse). |
labels | Partial<GanttLabels> | the gantt namespace | Every string, one key at a time over the nearest ComitorLocaleProvider. See Language. |
aria-label · className · div props | — | — | aria-label names the treegrid (default labels.label). className lands on the frame — give it a height there. |
GanttItem
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
idbắt buộc | string | — | Unique; dependencies refer to it. |
titlebắt buộc | string | — | The name in the left column and on the bar. |
start · end | string | null | — | yyyy-MM-dd, end inclusive: a one-day task has start === end. Either one missing makes the row unscheduled — no bar. An end before the start is read as the start; a string that is not a real date is read as missing. |
type | 'task' | 'summary' | 'milestone' | 'task' | task: a bar that drags and resizes. summary: a thin bar whose dates are yours to compute; it never drags. milestone: a diamond on start (end is ignored); it drags but does not resize. |
depth | number | 0 | Indents the name 16px a level and sets aria-level. |
hasChildren · expanded | boolean | — | Show the chevron, and which way it points. Only a display: the app filters items. |
progress | number | — | 0–100: a 2px strip just above the bar's bottom border, in the tone's -ink, and a sentence for screen readers. |
tone | GanttTone | 'neutral' | One of 11 names (see Tones and shapes). |
critical | boolean | — | On the critical path, as the app computed it: a 2px --destructive-ink border, and a sentence. |
readOnly | boolean | — | Locks this row only — a finished task, or one the user may not edit. |
GanttDependency
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
idbắt buộc | string | — | Unique among dependencies. |
frombắt buộc | string | — | The id of the task that comes first. |
tobắt buộc | string | — | The id of the task that follows. |
type | 'finish-to-start' | 'start-to-start' | 'finish-to-finish' | 'start-to-finish' | 'finish-to-start' | Where the arrow leaves and lands, and which rule the package checks (see The contract). |
critical | boolean | — | A solid 2px arrow in --destructive-ink. |
GanttItemChange and GanttDependencyDraft
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
id | string | — | The row that changed. |
start · end | string | — | The new dates, yyyy-MM-dd, end inclusive. Equal for a milestone. |
kind | 'move' | 'resize-start' | 'resize-end' | 'schedule' | — | How: the whole bar moved, one end moved, or an unscheduled row got its first dates from a click (1, 5, 14, 30 or 90 days by scale). |
GanttDependencyDraft | { from, to, type } | — | What onDependencyCreate receives — no id, which is yours to assign. type is always 'finish-to-start'. |
Known limitations
- Give it a height. The frame is a flex column and the rows scroll inside it. Without a height the Gantt grows to fit every row, its header scrolls away with the page, and the virtualiser has nothing to save.
- Rows do not move. A bar drags along its own row only; reordering and changing parent belong to Tree View beside it.
- The calendar is linear. Weekends and
nonWorkingDaysare shaded, not skipped: a task is as long as its calendar days, and a drag can land it on a Sunday. - Linking and scheduling have no keyboard path, and linking none on touch (see Keyboard). New links are always finish-to-start, and the package checks neither for duplicates nor for cycles.
- No baselines, no Ctrl + wheel zoom, and no way to scroll to a date from outside — the Today button lives in the built-in toolbar.
- Pointer changes are not announced. Only key presses reach the live region; a drag reports to
onItemChangesilently.
Accessibility
- A
treegridof two columns: arowheaderwith the name, and agridcellfor the axis that holds a visually hidden sentence — the dates and the number of days, the progress, “On the critical path”, “Starts before a task it depends on”. Rows carryaria-level,aria-expandedwhen they have children, andaria-selectedon the current one; the grid carriesaria-rowcount,aria-colcountand, when nothing can be edited,aria-readonly. - Bars, milestones and arrows are pictures, hidden from assistive technology; what they do is on the row. The keyboard instructions are a hidden paragraph wired with
aria-describedby, and each change made from the keyboard is spoken through a polite live region. - Colour is never alone. A bar stands off the page by its
-inkborder, at least 3:1 (WCAG 1.4.11); the critical path is also thicker; a broken link is also dashed. - On touch screens (
pointer: coarse) rows are 44px and bars 32px; resize handles are 44px wide and straddle the bar’s edge, and a bar under 96px keeps only its end handle, outside it, so the body is still there to hold. Bars settouch-action: none; empty parts of the axis still scroll. - What the app still has to do. Offer a keyboard way to add and remove a dependency and to give an unscheduled task its dates. Name the grid with
aria-labelwhen a page has more than one. Keeplangin step with the provider. And mark the critical path for real — a flag left stale after a drag is a wrong sentence read aloud.
Related
Tree View
The WBS outline beside the Gantt: indent, outdent and reorder there — the Gantt never moves a row. The same parentId list feeds both.
Tasks pattern
Where the Gantt and the board meet: the same tasks as a list, a board and a timeline.
Kanban
The same tasks by status instead of by date — drag between columns, with the same keyboard-first rules.
Timeline
Not a schedule: a vertical feed of what already happened to one record.
Data Grid
The same virtualised rows with cells you type into — start and end dates in a spreadsheet, when a bar is not precise enough.