p1-gantt
v0.5.0
Published
Reusable Gantt chart React component
Downloads
109
Maintainers
Readme
p1-gantt
A reusable, dependency-free React Gantt / timeline component written in TypeScript.
Render rows of time-positioned bars against a multi-level time axis, with
opt-in dragging, resizing, cross-row moves, snapping, background bands,
frozen rows, and fully custom rendering — all driven by a single config
object and styled through inline-style hooks (no CSS import required).
Features
- 🧱 Multi-level time header — stack any combination of minute/hour/day/week/month/quarter/year levels.
- ↔️ Drag to move & resize — enabled per-chart by providing callbacks, opt-out per item.
- ⤵️ Cross-row moves — drag a bar onto another row with a live drop-target highlight.
- 🧲 Snapping — snap drag deltas to any minute granularity.
- 🟦 Background bands — non-interactive ranges for breaks, holidays, unavailable slots.
- 📌 Frozen rows — pin rows to the top while the rest scroll.
- ➕ Empty-slot clicks — click empty grid to create new items, with a live ghost preview.
- 💬 Tooltips & custom renderers — bring your own JSX for bars, labels, headers, tooltips, bands.
- 🧬 TypeScript-first — generic over your row / item / band data types.
- 🔗 Dependency links —
config.linksdraws FS/SS/FF/SF arrows.linkSourceId+onLinkCreatefor click-to-link. Left-edge resize as well as right.
Installation
npm install p1-gantt
# or
yarn add p1-ganttreact and react-dom (v18 or v19) are peer dependencies.
Quick start
The chart fills its parent, so give the container an explicit height.
import { Gantt } from 'p1-gantt';
import type { GanttConfig } from 'p1-gantt';
export function Schedule() {
const config: GanttConfig = {
xAxis: {
start: new Date(2024, 0, 1, 8, 0),
end: new Date(2024, 0, 1, 18, 0),
cellWidth: 60,
levels: [
{ unit: 'hour', step: 2, labelFormat: 'HH:mm' },
{ unit: 'hour', labelFormat: 'HH' },
],
},
rows: [
{ id: 'room-1', data: { name: 'Room 1' } },
{ id: 'room-2', data: { name: 'Room 2' } },
],
items: [
{ id: 'a', rowId: 'room-1', start: new Date(2024, 0, 1, 9), end: new Date(2024, 0, 1, 11), label: 'Consult' },
{ id: 'b', rowId: 'room-2', start: new Date(2024, 0, 1, 13), end: new Date(2024, 0, 1, 15), label: 'Follow-up' },
],
renderers: {
renderLabel: (row) => <strong>{row.data.name}</strong>,
},
interactions: {
snapMinutes: 15,
// Providing onItemMove enables dragging; onItemResize enables resizing.
onItemMove: ({ item, nextStart, nextEnd, nextRowId }) => {
// Persist the change and update your own `items` state.
},
},
};
return (
<div style={{ height: 400 }}>
<Gantt config={config} />
</div>
);
}Controlled data: the component is presentational. It never mutates
itemsitself — youronItemMove/onItemResize/onEmptyClickhandlers persist the change and feed a newitemsarray back in. On the nextitemschange the bar settles into its new position seamlessly (the drop is optimistically pinned in the meantime).
Configuration
Everything is passed via the single config: GanttConfig prop.
GanttConfig
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| xAxis | TimeAxisConfig | — | Time range, cell width and header levels. |
| rows | GanttRow[] | — | One entry per lane. |
| items | GanttItem[] | — | The bars to position. |
| bands | GanttBand[] | [] | Non-interactive background ranges. |
| renderers | GanttRenderers | — | Custom JSX for bars, labels, headers, tooltips, bands. |
| styles | GanttStyleHooks | — | Per-element inline-style overrides. |
| interactions | GanttInteractions | — | Callbacks + drag/snap behavior. |
| rowHeight | number | 40 | Default row height (px). |
| headerHeight | number | 32 | Height of each header level (px). |
| labelWidth | number | 180 | Width of the sticky left label column (px). |
| tooltipPos | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Tooltip anchor side. |
| stackOverlaps | boolean | false | Lay overlapping items out into separate lanes instead of overlapping them. |
| expanded | boolean | false | When stacking, give each lane full row height (vs. a compact fanned-out offset). |
| onToggle | () => void | — | Invoked by a custom renderToggle / renderLabel control to flip expanded. |
TimeAxisConfig
| Field | Type | Description |
| --- | --- | --- |
| start / end | Date | Visible time range (half-open: end excluded). |
| cellWidth | number | Pixel width of one lowest-level cell. |
| levels | TimeHeaderLevel[] | Header rows, coarsest first; the last level sets the grid resolution. |
Each level: { unit, step?, labelFormat? }, where unit is one of
minute | hour | day | week | month | quarter | year. labelFormat
supports the tokens EEEE/EEE, d, MMMM/MMM, yyyy/yy, HH,
mm, and QQQ (formatted via Intl, en-US, 24-hour).
GanttRow / GanttItem / GanttBand
interface GanttRow<T> { id: string | number; data: T; height?: number; frozen?: boolean }
interface GanttItem<T> { id: string | number; rowId: string | number; start: Date; end: Date; label: string; movable?: boolean; resizable?: boolean; data?: T }
interface GanttBand<T> { id?: string | number; rowId: string | number | '*'; start: Date; end: Date; kind?: string; label?: string; data?: T }A band with rowId: '*' renders on every row.
GanttInteractions
| Field | Type | Description |
| --- | --- | --- |
| onItemClick | (item) => void | Click on a bar. |
| onItemMove | (req) => void \| Promise<…> | Defining this enables drag-to-move. Receives { item, nextStart, nextEnd, nextRowId }. |
| onItemResize | (req) => void \| Promise<…> | Defining this enables resize. Receives { item, nextEnd }. |
| onEmptyClick | (req) => void | Click on empty grid. Receives { rowId, start, end }; shows a ghost preview on hover. |
| snapMinutes | number | Snap drag deltas to this many minutes. Default: no snapping. |
| allowCrossRowMove | boolean | Allow dragging a bar to a different row. Default false. |
| minItemMinutes | number | Minimum duration on resize. Default: snapMinutes or 5. |
Drag/resize are gated by both the chart callback and the per-item
movable / resizable flags — set either to false to lock an item.
renderers & styles
renderers returns JSX for renderHeaderCell, renderLabel (receives the
row plus a { hasOverlaps, isExpanded, onToggle } context), renderToggle
(an expand/collapse control rendered in the header corner), renderItem,
renderTooltip, and renderBand. styles provides inline-style hooks
(getItemStyle, getRowStyle, getHeaderStyle, getBandStyle,
getDropTargetRowStyle, getEmptyClickPreviewStyle, …) plus border configs
(chartBorder, headerBorder, labelBorder, contentBorder). All are
optional and fall back to neutral defaults.
Notes & current limitations
- Container height is required — the chart is
height: 100%. - Overlapping items — by default, items sharing a row overlap visually.
Set
stackOverlapsto fan them into separate lanes, andexpandedto give each lane full row height. - Keyboard / a11y — interactions are currently pointer-only.
Development
nvm use # Node >= 18 (see .nvmrc)
yarn install
yarn build # bundle (Vite) + emit types (tsc)
yarn test # run unit tests (Vitest)
yarn test:watch # watch mode