Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Project timeline

New

A project tool’s Gantt screen: bars by status with progress and faces, dependency arrows, milestones, drag to reschedule, Weeks or Months.

project-timeline/project-timeline.tsx
Live · interactive
Maya

Timeline

Schedules, dependencies, and milestones across projects.

  • KickoffResearch readoutNavigation sign-offDesign freezeBeta releaseContent completeLaunch reviewGo liveRetrospective
  • Audit existing navigationSL
  • Summarize stakeholder interviewsMO
  • Footer and legal pagesPN
  • Design mobile onboarding1/3ET
  • Create homepage wireframes3/4SL
  • Fix broken anchor links on pricingJR
  • Finalize navigation1/3JR
  • Image optimization pipelineDK
  • Design responsive navigation1/2ET
  • Weekly design sync notesMO
  • Prepare design system tokensMO
  • Review responsive layoutsET
  • Build hero componentJR
  • Prepare developer handoffSL
  • Write homepage copyPN
  • Set up analytics eventsDK
  • Accessibility reviewET
  • QA pass on stagingJR
  • Offline cache spikeJR
  • Biometric sign-in2/5JR
  • Onboarding flow prototypeET
  • Push notification serviceDK
  • Triage beta tester feedbackMO
  • Store listing screenshotsPN
  • Audit color contrastSL
  • Dark mode palette3/5SL
  • Icon set, second passDK
  • Form field statesET

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A schedule of tasks that last days or weeks, where when things happen matters as much as what they are.
  • Work with dependencies: one task cannot start before another ends, and moving it has to push what waits on it.
  • Several projects or people over the same weeks, where grouping by project, status or assignee shows who is overloaded and when.
  • Planning by hand: dragging a bar, or nudging it a day with the arrow keys, is the way dates are changed.
  • Dates to hold the team to: milestones on their own row, and today’s line to read the plan against.

When not to use

  • Work that flows through stages rather than dates. Use a board with a column per status.
  • A short list of to-dos with due dates and no durations. Use a task list sorted by due date.
  • One person’s day or week of meetings. Use a calendar with hours.
  • Hundreds of tasks across a year. The rows and the arrows stop reading; show a roadmap of epics by quarter, or page the list.
  • Reporting progress to people who will not change the plan. A burn-up chart or a status summary says it in one look.

You can add this component with the shadcn CLI or by hand:

Into components/project-timeline of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/project-timeline.json

Either way it needs React 19.2 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

schedule.tsx
import { ProjectTimeline } from './project-timeline';export function Schedule() {  return (    <ProjectTimeline      workspace="Fernway Studio"      today="2026-10-13"      onTaskMove={(id, start, end) => console.log(id, start, end)}      autoplay={false}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

The timeline of a studio’s workspace, in an app window. A task list beside a day grid where each task is a bar coloured by its status, its progress filled in and its assignee’s face on it, with arrows to the tasks that wait on it and a row of milestones. Drag a bar, or press the arrow keys on it, and whatever depends on it is pushed along live; hover it for its card. Switch Weeks and Months, jump to today, filter, sort, group by project, status or assignee, fold a group, tick a task done. Light and dark.

  • The schedule is worked out on every render, never stored: each task keeps only the start it was planned for, and is drawn at the later of that and the day after every task it waits on ends (a few relaxation passes over the dependencies). A drag changes one planned start, so the tasks after it are pushed along live and fall back when it is dragged back.
  • A drag captures the pointer and snaps to whole days; a bar dragged against a task it waits on stays after it, and what is drawn becomes the plan on release. The arrow keys move a focused bar by a day (Shift by a week). Bars are placed by transform, so drags, pushes and view changes all slide on the compositor.
  • List and grid are one sheet scrolling both ways inside the chart: the header and the list column are sticky, so every row lines up with its bar with no scroll syncing. Month names stay at the left edge while their month is on show, and a bar’s face and name stay in view while the bar runs off the left.
  • The dependency arrows are one SVG of cubic curves from the end of a task to the start of the one that waits on it, worked out from the rows on show, so folding a group, filtering or sorting redraws them; the ones touching the hovered or dragged bar light up.
  • Weeks draws a 35 px column per day, Months an 11 px one with a label per week; the day at the left edge stays put through the switch. Weekends are shaded and weeks ruled by two repeating gradients on one layer, and today is a line; Today scrolls to it.
  • Grouping by project, status or assignee, sorting and the status filter are toolbar menus; a group folds to one summary bar from its first start to its last end. Ticking a task’s status circle marks it done and turns its bar green; unticking puts back the status it had.
  • Layout by container queries on the root: the sidebar is a column from 760 px (or a rail of icons, folded), a drawer under it, where the list column narrows to the task names and the toolbar wraps.
  • While on screen and untouched, a demo pointer hovers a bar, drags it three days so its dependants follow and back, switches to Months and back, jumps to today, ticks a task and folds a group. Any real input hands control to the reader, and what the demo changed is put back.
PropTypeDefaultDescription
workspacestring'Fernway Studio'The workspace’s name, in the sidebar and the breadcrumb.
titlestring'Timeline'The page’s title, also in the breadcrumb.
subtitlestring'Schedules, dependencies, and milestones across projects.'The line under the title.
todaystring'2026-10-13'Today, as YYYY-MM-DD: its line on the chart and where Today scrolls.
defaultView'weeks' | 'months''weeks'A column per day, or a narrow one with a label per week.
defaultGroup'project' | 'status' | 'assignee' | 'none''project'How the tasks are grouped.
defaultCollapsedbooleanfalseThe sidebar starts folded to its rail of icons (from 760 px of width).
framebooleantrueDraws the app window around it: its bar with the three lights and the title.
addressstring'Fernway Studio — Timeline'What the window’s bar says.
userNamestring'Maya'Who is signed in, top right.
onTaskMove(id: string, start: Date, end: Date) => void—Called when a task is dragged or moved by keys, with its new first and last day.
onTaskToggle(id: string, done: boolean) => void—Called when a task is ticked done or unticked.
defaultStateProjectTimelineState—Opens in this state, held still: view, group, sort, query (the task search), statuses (the filter), folded (group keys), menu (filter, sort or group open), tip (a task’s card open), moved (days added to tasks’ planned starts), done (task ids ticked), scrollTo (the day at the left edge, as month-date) and drawer. Turns the demo off.
autoplaybooleantrueWhile on screen and untouched, a demo pointer hovers and drags a bar, switches the scale, jumps to today, ticks a task and folds a group.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
classNamestring—Extra class for the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

project-timeline.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useEffectEvent,  useId,  useLayoutEffect,  useMemo,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,  type PointerEvent as ReactPointerEvent,  type ReactNode,} from 'react';import {  DAYS,  GROUP_PROJECTS,  MILESTONES,  MONTH_LONG,  PEOPLE,  PROJECTS,
project-timeline.module.css
/* Project timeline (project-timeline.tsx): a project tool's Gantt screen in an app window.   Near-black at night, the sidebar a step darker than the page, hairlines rather than fills, the   bars tinted by status (green done, amber in progress, violet in review, grey to do), each with   its progress filled in; by day the same in white and warm greys.   Colours are the tokens below, light-dark(light, dark): the component takes the color-scheme it   inherits from the page, or the one its colorScheme prop sets (data-color-scheme).   Geometry: a day is --day wide (set inline: 35.25 px in Weeks, 11 px in Months), a row --row   tall, the list column --list wide. Bars sit at translateX(start × day), so a drag, a push from   a dependency and a change of view all move them by transform.   Layout by container queries on the root: the sidebar is a column from 760 px (or a rail of   icons, folded), a drawer under it, where the list column narrows to the task names. */.root {  --app: #ffffff;  --side: #f7f7f5;  --line: #e6e6e2;  --divider: #efefec;  --hover: #f4f4f2;  --active: #ebebe8;  --ink: #1b1b19;  --ink-2: #5c5c58;
data.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The schedule the timeline shows: an invented design studio, its projects, its people, their * tasks with planned dates, what waits on what, and the milestones. All of it is made up. * * Dates are whole days counted from the first day of the chart (Monday, September 7, 2026), so * the arithmetic of a Gantt chart is plain integer sums; `day()` turns a calendar date into one. */import type { PATHS } from './icons';export type IconName = keyof typeof PATHS;/** The first day of the chart: a Monday, so weeks line up with the grid. */export const FIRST = Date.UTC(2026, 8, 7);/** How many days the chart spans: twelve weeks, to Sunday, November 29. */export const DAYS = 84;const DAY_MS = 86_400_000;/** The chart's day number of a calendar date (`month` is 1–12). */export const day = (month: number, date: number) =>  Math.round((Date.UTC(2026, month - 1, date) - FIRST) / DAY_MS);/** The calendar date of a chart day. */
icons.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The line icons, drawn on a 24-unit grid with a 1.7-unit round stroke in the text's colour. Each * is a list of path data; a circle is written as two arcs. The stylesheet sizes them (the lab page * would otherwise force every unsized SVG to 16 px). */const circle = (cx: number, cy: number, r: number) =>  `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0`;export const PATHS = {  home: ['M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4.5v-6h-5v6H5a1 1 0 0 1-1-1z'],  inbox: [    'M4 13.5 6.4 6.2A1.8 1.8 0 0 1 8.1 5h7.8a1.8 1.8 0 0 1 1.7 1.2L20 13.5V18a1.8 1.8 0 0 1-1.8 1.8H5.8A1.8 1.8 0 0 1 4 18z',    'M4 13.5h4.5l1.2 2.3h4.6l1.2-2.3H20',  ],  checkCircle: [circle(12, 12, 8.2), 'm8.6 12.2 2.3 2.3 4.6-4.8'],  star: ['m12 3.8 2.5 5.1 5.6.8-4 3.9 1 5.6L12 16.6l-5 2.6 1-5.6-4.1-3.9 5.6-.8z'],  search: [circle(11, 11, 6.3), 'm15.7 15.7 4.2 4.2'],  help: [circle(12, 12, 8.2), 'M9.7 9.6a2.4 2.4 0 1 1 3.3 2.2c-.6.3-1 .8-1 1.4v.4', 'M12 16.3v.1'],  bell: ['M6.3 16.5V11a5.7 5.7 0 0 1 11.4 0v5.5l1.5 1.5H4.8z', 'M10 20.5a2.2 2.2 0 0 0 4 0'],  overview: ['M4.5 4.5h6v6h-6z', 'M13.5 4.5h6v4h-6z', 'M13.5 11.5h6v8h-6z', 'M4.5 13.5h6v6h-6z'],  folder: [
demo-pointer.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the * hand are HTML boxes that swap by their visibility: a change of style inside an SVG would lay the * SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = {
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(
  • Each bar is a button named with its task and dates; the arrow keys move it by a day (Shift by a week), and focusing it opens its card, linked by aria-describedby. Every move is announced with the new dates and how many dependent tasks moved.
  • Each task’s status circle is a checkbox named after the task; group headers are buttons with aria-expanded. Milestones are images named with their title and date; faces carry the person’s name.
  • The toolbar is a labelled toolbar: Filter, Sort and Group are menu buttons with checkbox or radio menus (arrow keys, Home, End, Escape returns focus); Weeks and Months are a radio group.
  • / focuses the task search and Ctrl K (or ⌘K) the jump box, which scrolls to the first matching task. Changes of view, grouping, ticks and jumps are announced in a polite live region.
  • The sidebar is a labelled navigation landmark with aria-current on the page; under 760 px it is a drawer that Escape closes. Reduced motion removes the transitions and stops the demo; the demo pointer and the chart’s art are hidden from assistive technology.