# Gantt Chart

> Shows items as bars across a date range, in groups, with a fixed name column, week ticks, and a today marker. Use it to see when work runs and how far along it is.

- Name: `gantt-chart`
- Status: beta
- Categories: charts, data, dates
- File: the install writes `gantt-chart.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/glide-highlight`, `@arbokui/skeleton`, `@arbokui/tooltip`. The install adds them.
- Gallery: https://arbokui.com/components/gantt-chart
- Registry item: https://arbokui.com/r/gantt-chart.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/gantt-chart
   ```

## Use it for

- Work that spans days or weeks, like projects by stage, where start, end, and progress matter.
- Pass groups, each with its items. An item takes a label, start and end days, and progress from 0 to 1.
- Pass days as ISO strings, like '2026-10-13'.
- Pass onItemOpen so each bar is a button that opens its item, like a Sheet.
- Put a caption on a bar for a short count, like '45% · 9/20'. It shows beside the bar, so the fill reads whole.
- The chart has loading and empty states. A failed load is yours: show a Notice with a retry in its place.
- To build your own rows, like a list that turns into this chart, wrap them in GanttScale and use GanttHeader, GanttTrack, GanttGrid, and GanttToday for the date side.

## Avoid it for

- Times within one day, like bookings. Use Time Grid.
- A list of events with no length. Use Timeline.
- One value over time. Use LineChart or AreaChart.
- Dragging bars to new dates. Bars open, they do not move.

## Props

- `groups`: `{ id: string; title: ReactNode; items: GanttItem[] }[]`. The rows, in groups, like stages. Each group shows its title and count, then its items. A group with no items still shows its title.
- `GanttItem`: `{ id: string; label: string; meta?: ReactNode; start: string | Date; end: string | Date; progress?: number; tone?: "neutral" | "brand" | "danger" | "success" | "warning"; caption?: ReactNode }`. One row. The bar runs from the start of start to the end of end. progress, from 0 to 1, fills that share in tone, neutral by default. caption shows beside the bar, and defaults to the progress as a percent.
- `aria-label`: `string`. Names the chart for screen readers.
- `start`: `string | Date`. Default `the earliest item's start`. The first day shown.
- `end`: `string | Date`. Default `the latest item's end`. The last day shown.
- `today`: `string | Date`. Places the today marker, a pill with the date in the header and a line down the chart. Left out, there is no marker. Pass it from the server, so the first render matches.
- `locale`: `string`. Default `"en-US"`. The locale for dates and percents. One it does not know falls back to en-US.
- `timeZone`: `string`. Default `"UTC"`. The time zone that puts a Date, or a string with a time, on its day. ISO days, like '2026-10-13', stay as they are. One it does not know falls back to UTC.
- `loading`: `boolean`. Default `false`. Shows placeholder rows while the items load.
- `empty`: `ReactNode`. Default `"Nothing scheduled yet."`. What shows when no group has items. Give it a next step, like a button that adds one.
- `onItemOpen`: `(id: string) => void`. Makes each bar a button that opens its item, and lets a press on its row open it. Called with the item's id.
- `GanttScale`: `GanttScaleProps`. Part. Holds start, end, today, locale, and timeZone for the parts inside, and sets the layout variables. Put it around your own scroll box and rows. It is a size container.
- `GanttHeader`: `GanttHeaderProps`. Part. The dates along the top, a tick at each Monday. A tick near today gives way to the today pill. The right cell of the header row.
- `GanttTrack`: `GanttTrackProps`. Part. The right cell of one row, with its item's bar. Takes item, index (the row's place from the top across every group, which sets when its bar arrives), and onItemOpen.
- `GanttGrid`: `GanttGridProps`. Part. Faint week lines over the date side. Put it first inside the positioned box that holds the rows.
- `GanttToday`: `GanttTodayProps`. Part. The today marker over the date side. It arrives last: a pill with the date in the header, then the line draws down over the bars. Put it beside GanttGrid.
- `className`: `string`. Classes for the root. The layout reads variables you can set here: --gantt-head-h (2.5rem), --gantt-group-h (2.5rem), --gantt-row-h (2.75rem), --gantt-name-w (12rem, down to 8.5rem in a narrow box), --gantt-day-w (0.625rem, the least width of a day; a wider box stretches the days), and --gantt-bg (the page color, behind the fixed names).

## Accessibility

- The chart is a region named by aria-label, around a table: a header row, a row group per group with its title as a row header, and a row per item with its name as the row header.
- With onItemOpen, each bar is a button named by its item, its dates, and its progress, like 'Payments page, Oct 19 – Nov 6, 45% done'. Focus scrolls it clear of the fixed names.
- Without onItemOpen, each row's cell says the same in hidden text. When the dates scroll, the scroll box is one Tab stop, so a keyboard can scroll them. With buttons inside, it is no stop of its own.
- Hover or keyboard focus on a bar shows its name, dates, progress, and caption in a tooltip. One tooltip shows at a time.
- A name cut off by the column shows in full in a tooltip on hover, or on a tap when the bars are not buttons. Screen readers read the whole name.
- The week lines and the today marker are hidden from screen readers. The header says the range and today's date.
- In forced colors, each bar keeps an outline, the done part and the today marker take the text color, the today line widens, and the week lines turn GrayText, so today stays the strongest line.
- With reduced motion, bars and the today line fade in place instead of growing.

## Examples

### Items by stage, with today marked

```tsx
<GanttChart
  aria-label="Work by stage"
  start="2026-10-05"
  end="2026-11-15"
  today="2026-10-30"
  groups={[
    {
      id: "doing",
      title: "In progress",
      items: [
        { id: "payments", label: "Payments page", meta: "Web", start: "2026-10-19", end: "2026-11-06", progress: 0.45, tone: "brand", caption: "45% · 9/20" },
        { id: "help", label: "Help center", meta: "Web", start: "2026-10-13", end: "2026-10-31", progress: 0.9 },
      ],
    },
  ]}
  onItemOpen={(id) => openItem(id)}
/>
```

### Dates in another locale, with Date values put on their day in a time zone

```tsx
<GanttChart
  aria-label="Work by stage"
  locale="de-DE"
  timeZone="Europe/Berlin"
  today={new Date("2026-10-30T09:00:00Z")}
  groups={[
    {
      id: "doing",
      title: "In progress",
      items: [{ id: "payments", label: "Payments page", start: new Date("2026-10-19T08:00:00Z"), end: new Date("2026-11-06T17:00:00Z") }],
    },
  ]}
/>
```

### Your own name column, with the date side from the parts

```tsx
<GanttScale start="2026-10-05" end="2026-11-15" today="2026-10-30" locale="en-GB" timeZone="Europe/London">
  <div className="overflow-x-auto">
    <div role="table" aria-label="Work" className="relative w-max min-w-full">
      <GanttGrid />
      <GanttToday />
      <div role="row" className="grid h-(--gantt-head-h) grid-cols-[var(--gantt-name-w)_minmax(0,1fr)]">
        <div role="columnheader" className="sticky start-0 z-10 bg-(--gantt-bg)">Name</div>
        <GanttHeader />
      </div>
      {items.map((item, index) => (
        <div key={item.id} role="row" className="grid h-(--gantt-row-h) grid-cols-[var(--gantt-name-w)_minmax(0,1fr)]">
          <div role="rowheader" className="sticky start-0 z-10 bg-(--gantt-bg)">{item.label}</div>
          <GanttTrack item={item} index={index} onItemOpen={openItem} />
        </div>
      ))}
    </div>
  </div>
</GanttScale>
```
