Skip to content
ilinxa/pro-ui

Progress Timeline

alphav0.2.0

Horizontal progress bar with a current-position marker and start, state-aware center, and end captions — derives its state from three dates.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

Context

Use for any time-bound progress display — registration windows, sprints, sales countdowns, course completion windows, fundraising deadlines. Public helper `deriveTimelineState` exported alongside so consumers can derive state without rendering (header counters, calendar coloring, deterministic tests). Migration origin: kasder events/[id]/page.tsx Time Bar block.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/progress-timeline

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/progress-timeline-fixtures

Preview

Zaman Çizelgesi

Kayıt Başlangıcı29 gün kaldıEtkinlik Günü

Demo source

demo.tsxtsx

Usage

When to use

Reach for ProgressTimeline when you need to communicate progress through a time-bound window — registration windows, sprints, sales countdowns, course completion windows, fundraising deadlines. The component renders a horizontal progress bar with a marker dot at the current percentage + 3 captions (start / dynamic state-aware center / end), auto-deriving a 3-state machine (before / active / after) from start + end + now.

Minimal example

import { ProgressTimeline } from "@/components/progress-timeline";

<ProgressTimeline
  start="2026-04-01"
  end="2026-06-30"
  heading="Registration Window"
/>;

Public helper kernel — derive state without rendering

The kernel is a pure function exported alongside the component. Use it for header counters, calendar coloring, status filter logic, deterministic tests — without rendering the bar:

import {
  ProgressTimeline,
  deriveTimelineState,
  type TimelineState,
} from "@/components/progress-timeline";

// Header counter — how many windows are currently open?
const activeCount = events.filter(
  (e) => deriveTimelineState(e.regStart, e.regEnd).status === "active",
).length;

// Calendar day-cell coloring
function isWithinWindow(start: string, end: string, day: Date) {
  return deriveTimelineState(start, end, day).status === "active";
}

Localizing labels

Pass a labels object. Each text label accepts a string OR a function (state: TimelineState) => ReactNode for dynamic content driven by the derived state:

<ProgressTimeline
  start={event.registrationOpens}
  end={event.date}
  labels={{
    startLabel: "Kayıt Başlangıcı",
    endLabel: "Etkinlik Günü",
    beforeText: (state) => `${state.daysToStart} gün sonra başlıyor`,
    activeText: (state) => `${state.daysToEnd} gün kaldı`,
    afterText: "Etkinlik Sona Erdi",
  }}
/>

renderCenterLabel — full takeover

For full control of the center caption (e.g. compose percent + days), use renderCenterLabel — receives the derived TimelineState:

<ProgressTimeline
  start={start}
  end={end}
  renderCenterLabel={(state) => (
    <span>
      <strong>{Math.round(state.percent)}%</strong> · {state.daysToEnd} days left
    </span>
  )}
/>

value escape hatch — non-time progress

For non-time-based progress (course completion %, fundraising %, etc.), pass value (0-100) — overrides the time-derived bar fill. The state machine still derives from start/end so the captions stay meaningful:

<ProgressTimeline
  start={course.startDate}
  end={course.endDate}
  value={courseCompletion}
  labels={{
    startLabel: "Module 1",
    endLabel: "Module 12",
    activeText: () => `${courseCompletion}% complete`,
  }}
/>

Live-clock host — minute-accurate state flips

function LiveTimeline({ event }) {
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 60_000);
    return () => clearInterval(id);
  }, []);
  return (
    <ProgressTimeline start={event.start} end={event.end} now={now} />
  );
}

The component has no internal setInterval — consumer drives the cadence (1-minute / 5-minute / 1-hour windows your call).

Notes

  • start + end are required even when value is supplied — captions need anchors and the state machine needs date boundaries.
  • Invalid dates clamp gracefully (no crash); out-of-window times render as 0% / 100%.
  • headingAs defaults to h3 (timelines are typically nested under a page h2 section). Bump via headingAs="h2" when standalone.
  • headingIcon defaults to Timer from lucide-react. Pass headingIcon={null} to omit, or pass anyComponentType to swap.
  • marker="none" hides the dot; useful for dense contexts.
  • The marker dot extends slightly past the bar at 0% / 100% (half-dot width) — by design; the dot represents the position, not the bar fill.

Features

  • Horizontal progress bar with marker dot at current %
  • 3-state state machine (before / active / after) auto-derived
  • Public helper kernel — deriveTimelineState pure function
  • Dynamic center label — string OR (state) => ReactNode
  • Frame toggle (framed/bare) + marker toggle (dot/none)
  • Optional heading with configurable level + icon
  • value escape hatch for non-time-based progress
  • now injection for deterministic / live-clock hosts
  • statusOverride for preview / what-if states
  • i18n via labels object (6 keys)
  • WCAG — Radix Progress role=progressbar + aria-valuenow
  • Status-conditional bar fill + marker color — before (muted gray), active (lime), after (mid-gray); pairs with center-text differentiation
  • Soft-failure on invalid dates

Tags

progress-timelineprogresstimelinecountdownevents

Dependencies

shadcn primitives: progress
npm peer deps: lucide-react@^1.11.0