Progress Timeline
alphav0.2.0Horizontal progress bar with a current-position marker and start, state-aware center, and end captions — derives its state from three dates.
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
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/progress-timelineAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/progress-timeline-fixturesPreview
Zaman Çizelgesi
Demo source
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+endare required even whenvalueis 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%.
headingAsdefaults toh3(timelines are typically nested under a pageh2section). Bump viaheadingAs="h2"when standalone.headingIcondefaults toTimerfrom lucide-react. PassheadingIcon={null}to omit, or pass anyComponentTypeto 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