Gantt
The Gantt chart is a powerful tool for visualizing project schedules and tracking the progress of tasks. It provides a clear, hierarchical view of tasks, allowing you to easily identify manage project timelines.
Installation
npx love-ui@latest add ganttUsage
import { GanttProvider, GanttSidebar, GanttSidebarHeader, GanttSidebarGroup, GanttSidebarItem, GanttTimeline, GanttHeader, GanttColumns, GanttFeatureList, GanttFeatureRow, GanttToday, GanttMarker } from "@/components/gantt"<GanttProvider features={features} range="daily">
<GanttSidebar>
<GanttSidebarHeader />
{groups.map((group) => (
<GanttSidebarGroup key={group.id} group={group}>
{group.features.map((feature) => (
<GanttSidebarItem key={feature.id} feature={feature} />
))}
</GanttSidebarGroup>
))}
</GanttSidebar>
<GanttTimeline>
<GanttHeader />
<GanttColumns />
<GanttFeatureList>
{groups.map((group) => (
<GanttFeatureRow key={group.id} features={group.features} />
))}
</GanttFeatureList>
<GanttToday />
</GanttTimeline>
</GanttProvider>Features
- Resizable and draggable timeline items
- Markers to highlight important dates
- Today marker to highlight the current date
- Create marker trigger to create a new marker
- Grouping of features
- Multiple items per row for scenarios like hotel reservations, resource booking, etc.
Examples
Multiple items on one row
Perfect for hotel reservations, resource scheduling, or any scenario where multiple items share the same category but have different time periods.
Issues
Duration
Hotel Reservations
Room 101
3 months
Room 104
17 days
Room 105
3 months
Room 102
18 days
Room 103
25 days
2024
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
2025
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
2026
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
Garry Dicki - Conference
GaDr. Willard Maggio - Business Trip
DrKaren Hirthe-Boyer - Vacation
KaWoodrow Marvin - Business Trip
WoDr. Willard Maggio - Vacation
DrLisa Hauck DDS - Business Trip
LiWoodrow Marvin - Conference
WoKaren Hirthe-Boyer - Business Trip
KaDelores Klocko - Weekend Getaway
DeDr. Sonja Champlin - Conference
DrKaren Hirthe-Boyer - Vacation
KaJesus Kshlerin I - Conference
Je"use client";
import { faker } from "@faker-js/faker";
import {
GanttCreateMarkerTrigger,
GanttFeatureList,
GanttFeatureListGroup,
GanttFeatureRow,
GanttHeader,
GanttMarker,
GanttProvider,
GanttSidebar,
GanttSidebarGroup,
GanttSidebarItem,
GanttTimeline,
GanttToday,
} from "../../../../../packages/gantt";
// @ts-ignore: No type declarations for 'lodash.groupby'
import groupBy from "lodash.groupby";
import { EyeIcon, LinkIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
// Seed faker to ensure consistent data between server and client
faker.seed(123);
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
const statuses = [
{ id: faker.string.uuid(), name: "Confirmed", color: "#10B981" },
{ id: faker.string.uuid(), name: "Pending", color: "#F59E0B" },
{ id: faker.string.uuid(), name: "Cancelled", color: "#EF4444" },
];
const guests = Array.from({ length: 8 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
image: faker.image.avatar(),
}));
// Hotel rooms
const hotelRooms = Array.from({ length: 5 })
.fill(null)
.map((_, index) => ({
id: faker.string.uuid(),
name: `Room ${101 + index}`,
}));
// Generate hotel reservations - multiple guests can book the same room for different periods
const hotelReservations = Array.from({ length: 12 })
.fill(null)
.map(() => {
const startDate = faker.date.future({ years: 0.3, refDate: new Date() });
const endDate = faker.date.future({ years: 0.1, refDate: startDate });
const room = faker.helpers.arrayElement(hotelRooms);
const guest = faker.helpers.arrayElement(guests);
return {
id: faker.string.uuid(),
name: `${guest.name} - ${faker.helpers.arrayElement(["Business Trip", "Vacation", "Conference", "Weekend Getaway"])}`,
startAt: startDate,
endAt: endDate,
status: faker.helpers.arrayElement(statuses),
lane: room.id, // This groups reservations by room
// Store additional data that's not part of core GanttFeature
metadata: {
guest,
room,
group: { name: "Hotel Reservations" },
},
};
});
const exampleMarkers = Array.from({ length: 3 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
date: faker.date.future({ years: 0.2, refDate: new Date() }),
label: faker.helpers.arrayElement([
"Holiday Period",
"Conference Week",
"Peak Season",
]),
className: faker.helpers.arrayElement([
"bg-blue-100 text-blue-900",
"bg-green-100 text-green-900",
"bg-purple-100 text-purple-900",
]),
}));
const Example = () => {
const [reservations, setReservations] = useState(hotelReservations);
// Group reservations by room (lane), then by group
const groupedReservations = groupBy(reservations, "metadata.group.name");
const roomGroupedReservations = Object.fromEntries(
Object.entries(groupedReservations).map(
([groupName, groupReservations]) => [
groupName,
groupBy(groupReservations, "lane"),
]
)
);
const handleViewReservation = (id: string) =>
console.log(`Reservation selected: ${id}`);
const handleCopyLink = (id: string) => console.log(`Copy link: ${id}`);
const handleRemoveReservation = (id: string) =>
setReservations((prev) =>
prev.filter((reservation) => reservation.id !== id)
);
const handleRemoveMarker = (id: string) =>
console.log(`Remove marker: ${id}`);
const handleCreateMarker = (date: Date) =>
console.log(`Create marker: ${date.toISOString()}`);
const handleMoveReservation = (
id: string,
startAt: Date,
endAt: Date | null
) => {
if (!endAt) {
return;
}
setReservations((prev) =>
prev.map((reservation) =>
reservation.id === id ? { ...reservation, startAt, endAt } : reservation
)
);
console.log(`Move reservation: ${id} from ${startAt} to ${endAt}`);
};
const handleAddReservation = (date: Date) =>
console.log(`Add reservation: ${date.toISOString()}`);
return (
<GanttProvider
className="border"
onAddItem={handleAddReservation}
range="monthly"
zoom={100}
>
<GanttSidebar>
{Object.entries(roomGroupedReservations).map(
([groupName, roomReservations]) => (
<GanttSidebarGroup key={groupName} name={groupName}>
{Object.entries(roomReservations).map(
([roomId, roomReservationList]) => {
const room = hotelRooms.find((r) => r.id === roomId);
// Create a representative feature for the sidebar
const representativeReservation = {
id: roomId,
name: room?.name || "Unknown Room",
startAt: new Date(
Math.min(
...roomReservationList.map((r) => r.startAt.getTime())
)
),
endAt: new Date(
Math.max(
...roomReservationList.map((r) => r.endAt.getTime())
)
),
status: roomReservationList[0].status,
};
return (
<GanttSidebarItem
feature={representativeReservation}
key={roomId}
onSelectItem={() => handleViewReservation(roomId)}
/>
);
}
)}
</GanttSidebarGroup>
)
)}
</GanttSidebar>
<GanttTimeline>
<GanttHeader />
<GanttFeatureList>
{Object.entries(roomGroupedReservations).map(
([groupName, roomReservations]) => (
<GanttFeatureListGroup key={groupName}>
{Object.entries(roomReservations).map(
([roomId, roomReservationList]) => (
<div key={roomId}>
<GanttFeatureRow
features={roomReservationList}
onMove={handleMoveReservation}
>
{(reservation) => (
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="flex w-full items-center gap-2">
<p className="flex-1 truncate text-xs">
{reservation.name}
</p>
{(reservation as any).metadata?.guest && (
<Avatar className="h-4 w-4">
<AvatarImage
src={
(reservation as any).metadata.guest
.image
}
/>
<AvatarFallback>
{(
reservation as any
).metadata.guest.name?.slice(0, 2)}
</AvatarFallback>
</Avatar>
)}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
className="flex items-center gap-2"
onClick={() =>
handleViewReservation(reservation.id)
}
>
<EyeIcon
className="text-muted-foreground"
size={16}
/>
View reservation
</ContextMenuItem>
<ContextMenuItem
className="flex items-center gap-2"
onClick={() => handleCopyLink(reservation.id)}
>
<LinkIcon
className="text-muted-foreground"
size={16}
/>
Copy link
</ContextMenuItem>
<ContextMenuItem
className="flex items-center gap-2 text-destructive"
onClick={() =>
handleRemoveReservation(reservation.id)
}
>
<TrashIcon size={16} />
Cancel reservation
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)}
</GanttFeatureRow>
</div>
)
)}
</GanttFeatureListGroup>
)
)}
</GanttFeatureList>
{exampleMarkers.map((marker) => (
<GanttMarker
key={marker.id}
{...marker}
onRemove={handleRemoveMarker}
/>
))}
<GanttToday />
<GanttCreateMarkerTrigger onCreateMarker={handleCreateMarker} />
</GanttTimeline>
</GanttProvider>
);
};
export default Example;
Without a sidebar
2024
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
2025
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
2026
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
"use client";
import { faker } from "@faker-js/faker";
import {
GanttCreateMarkerTrigger,
GanttFeatureItem,
GanttFeatureList,
GanttFeatureListGroup,
GanttHeader,
GanttMarker,
GanttProvider,
GanttTimeline,
GanttToday,
} from "../../../../../packages/gantt";
import groupBy from "lodash.groupby";
import { EyeIcon, LinkIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
// Seed faker to ensure consistent data between server and client
faker.seed(123);
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
const statuses = [
{ id: faker.string.uuid(), name: "Planned", color: "#6B7280" },
{ id: faker.string.uuid(), name: "In Progress", color: "#F59E0B" },
{ id: faker.string.uuid(), name: "Done", color: "#10B981" },
];
const users = Array.from({ length: 4 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
image: faker.image.avatar(),
}));
const exampleGroups = Array.from({ length: 6 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleProducts = Array.from({ length: 4 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleInitiatives = Array.from({ length: 2 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleReleases = Array.from({ length: 3 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleFeatures = Array.from({ length: 20 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
startAt: faker.date.past({ years: 0.5, refDate: new Date() }),
endAt: faker.date.future({ years: 0.5, refDate: new Date() }),
status: faker.helpers.arrayElement(statuses),
owner: faker.helpers.arrayElement(users),
group: faker.helpers.arrayElement(exampleGroups),
product: faker.helpers.arrayElement(exampleProducts),
initiative: faker.helpers.arrayElement(exampleInitiatives),
release: faker.helpers.arrayElement(exampleReleases),
}));
const exampleMarkers = Array.from({ length: 6 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
date: faker.date.past({ years: 0.5, refDate: new Date() }),
label: capitalize(faker.company.buzzPhrase()),
className: faker.helpers.arrayElement([
"bg-blue-100 text-blue-900",
"bg-green-100 text-green-900",
"bg-purple-100 text-purple-900",
"bg-red-100 text-red-900",
"bg-orange-100 text-orange-900",
"bg-teal-100 text-teal-900",
]),
}));
const Example = () => {
const [features, setFeatures] = useState(exampleFeatures);
const groupedFeatures = groupBy(features, "group.name");
const sortedGroupedFeatures = Object.fromEntries(
Object.entries(groupedFeatures).sort(([nameA], [nameB]) =>
nameA.localeCompare(nameB)
)
);
const handleViewFeature = (id: string) =>
console.log(`Feature selected: ${id}`);
const handleCopyLink = (id: string) => console.log(`Copy link: ${id}`);
const handleRemoveFeature = (id: string) =>
setFeatures((prev) => prev.filter((feature) => feature.id !== id));
const handleRemoveMarker = (id: string) =>
console.log(`Remove marker: ${id}`);
const handleCreateMarker = (date: Date) =>
console.log(`Create marker: ${date.toISOString()}`);
const handleMoveFeature = (id: string, startAt: Date, endAt: Date | null) => {
if (!endAt) {
return;
}
setFeatures((prev) =>
prev.map((feature) =>
feature.id === id ? { ...feature, startAt, endAt } : feature
)
);
console.log(`Move feature: ${id} from ${startAt} to ${endAt}`);
};
const handleAddFeature = (date: Date) =>
console.log(`Add feature: ${date.toISOString()}`);
return (
<GanttProvider
className="border"
onAddItem={handleAddFeature}
range="monthly"
zoom={100}
>
<GanttTimeline>
<GanttHeader />
<GanttFeatureList>
{Object.entries(sortedGroupedFeatures).map(([group, features]) => (
<GanttFeatureListGroup key={group}>
{features.map((feature) => (
<div className="flex" key={feature.id}>
<ContextMenu>
<ContextMenuTrigger asChild>
<button
onClick={() => handleViewFeature(feature.id)}
type="button"
>
<GanttFeatureItem
onMove={handleMoveFeature}
{...feature}
>
<p className="flex-1 truncate text-xs">
{feature.name}
</p>
{feature.owner && (
<Avatar className="h-4 w-4">
<AvatarImage src={feature.owner.image} />
<AvatarFallback>
{feature.owner.name?.slice(0, 2)}
</AvatarFallback>
</Avatar>
)}
</GanttFeatureItem>
</button>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
className="flex items-center gap-2"
onClick={() => handleViewFeature(feature.id)}
>
<EyeIcon className="text-muted-foreground" size={16} />
View feature
</ContextMenuItem>
<ContextMenuItem
className="flex items-center gap-2"
onClick={() => handleCopyLink(feature.id)}
>
<LinkIcon className="text-muted-foreground" size={16} />
Copy link
</ContextMenuItem>
<ContextMenuItem
className="flex items-center gap-2 text-destructive"
onClick={() => handleRemoveFeature(feature.id)}
>
<TrashIcon size={16} />
Remove from roadmap
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
))}
</GanttFeatureListGroup>
))}
</GanttFeatureList>
{exampleMarkers.map((marker) => (
<GanttMarker
key={marker.id}
{...marker}
onRemove={handleRemoveMarker}
/>
))}
<GanttToday />
<GanttCreateMarkerTrigger onCreateMarker={handleCreateMarker} />
</GanttTimeline>
</GanttProvider>
);
};
export default Example;
Read-only version
Issues
Duration
Architect ubiquitous AI
Integrate real-time experiences
4 months
Aggregate global markets
6 months
Deploy turn-key paradigms
about 1 month
Deliver best-of-breed solutions
Unleash killer infrastructures
4 months
Gamify cross-platform channels
9 months
Exploit ubiquitous solutions
Extend front-end niches
11 months
Grow innovative technologies
Evolve best-of-breed markets
8 months
Collaborate one-to-one applications
11 months
Visualize 24/7 users
7 months
Collaborate holistic large language models
2 months
Aggregate B2B ROI
8 months
Productize killer large language models
7 months
Redefine impactful architectures
4 months
Monetize decentralized communities
Engage AI-driven infrastructures
4 months
Embrace end-to-end AI
8 months
Incubate sticky mindshare
10 months
Utilize quantum schemas
Grow dynamic content
5 months
Redefine user-centric relationships
6 months
Repurpose cross-platform functionalities
6 months
Extend sticky channels
8 months
2024
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
2025
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
2026
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
Integrate real-time experiences
Aggregate global markets
Deploy turn-key paradigms
Unleash killer infrastructures
Gamify cross-platform channels
Extend front-end niches
Evolve best-of-breed markets
Collaborate one-to-one applications
Visualize 24/7 users
Collaborate holistic large language models
Aggregate B2B ROI
Productize killer large language models
Redefine impactful architectures
Engage AI-driven infrastructures
Embrace end-to-end AI
Incubate sticky mindshare
Grow dynamic content
Redefine user-centric relationships
Repurpose cross-platform functionalities
Extend sticky channels
"use client";
import { faker } from "@faker-js/faker";
import {
GanttFeatureItem,
GanttFeatureList,
GanttFeatureListGroup,
GanttHeader,
GanttMarker,
GanttProvider,
GanttSidebar,
GanttSidebarGroup,
GanttSidebarItem,
GanttTimeline,
GanttToday,
} from "../../../../../packages/gantt";
import groupBy from "lodash.groupby";
// Seed faker to ensure consistent data between server and client
faker.seed(123);
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
const statuses = [
{ id: faker.string.uuid(), name: "Planned", color: "#6B7280" },
{ id: faker.string.uuid(), name: "In Progress", color: "#F59E0B" },
{ id: faker.string.uuid(), name: "Done", color: "#10B981" },
];
const users = Array.from({ length: 4 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
image: faker.image.avatar(),
}));
const exampleGroups = Array.from({ length: 6 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleProducts = Array.from({ length: 4 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleInitiatives = Array.from({ length: 2 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleReleases = Array.from({ length: 3 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
}));
const exampleFeatures = Array.from({ length: 20 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
name: capitalize(faker.company.buzzPhrase()),
startAt: faker.date.past({ years: 0.5, refDate: new Date() }),
endAt: faker.date.future({ years: 0.5, refDate: new Date() }),
status: faker.helpers.arrayElement(statuses),
owner: faker.helpers.arrayElement(users),
group: faker.helpers.arrayElement(exampleGroups),
product: faker.helpers.arrayElement(exampleProducts),
initiative: faker.helpers.arrayElement(exampleInitiatives),
release: faker.helpers.arrayElement(exampleReleases),
}));
const exampleMarkers = Array.from({ length: 6 })
.fill(null)
.map(() => ({
id: faker.string.uuid(),
date: faker.date.past({ years: 0.5, refDate: new Date() }),
label: capitalize(faker.company.buzzPhrase()),
className: faker.helpers.arrayElement([
"bg-blue-100 text-blue-900",
"bg-green-100 text-green-900",
"bg-purple-100 text-purple-900",
"bg-red-100 text-red-900",
"bg-orange-100 text-orange-900",
"bg-teal-100 text-teal-900",
]),
}));
const Example = () => {
const groupedFeatures = groupBy(exampleFeatures, "group.name");
const sortedGroupedFeatures = Object.fromEntries(
Object.entries(groupedFeatures).sort(([nameA], [nameB]) =>
nameA.localeCompare(nameB)
)
);
return (
<GanttProvider className="border" range="monthly" zoom={100}>
<GanttSidebar>
{Object.entries(sortedGroupedFeatures).map(([group, features]) => (
<GanttSidebarGroup key={group} name={group}>
{features.map((feature) => (
<GanttSidebarItem feature={feature} key={feature.id} />
))}
</GanttSidebarGroup>
))}
</GanttSidebar>
<GanttTimeline>
<GanttHeader />
<GanttFeatureList>
{Object.entries(sortedGroupedFeatures).map(([group, features]) => (
<GanttFeatureListGroup key={group}>
{features.map((feature) => (
<div className="flex" key={feature.id}>
<GanttFeatureItem {...feature} />
</div>
))}
</GanttFeatureListGroup>
))}
</GanttFeatureList>
{exampleMarkers.map((marker) => (
<GanttMarker key={marker.id} {...marker} />
))}
<GanttToday />
</GanttTimeline>
</GanttProvider>
);
};
export default Example;