The Future of Web Dev
The Future of Web Dev
Lifeline: Animated Timeline Component for Next.js
Add a horizontal desktop timeline and vertical mobile timeline to Next.js with editable shadcn source files, media, people, and event data.

Lifeline is a shadcn/ui timeline component registry that allows you to create chronological stories as a horizontal, scroll-scrubbed rail on desktop and a vertical timeline on mobile.
You can use the components for personal histories, company archives, product releases, tournament records, and editorial story pages with event text, links, portraits, logos, images, video, and lightbox media.
Features
- Scrubs a horizontal timeline with the mouse wheel, trackpad, pointer drag, or arrow keys on desktop.
- Switches to a vertical scrolling layout below the Tailwind CSS
mdbreakpoint. - Animates the rail and markers on first view, with a reduced-motion fallback.
- Accepts text, links, hover images, looping video previews, badges, company marks, mentors, and people met.
- Places draggable photo cards along the rail and opens them in a lightbox.
- Runs as a full-page experience or an embedded module inside a longer page.
- Installs as an editable local source through the shadcn CLI.
Use Cases
- A personal portfolio maps education, roles, projects, mentors, and major career changes across a single visual history.
- Company history pages connect founding dates, product launches, acquisitions, leadership changes, and press images.
- Product teams document a release sequence with linked announcements, screenshots, videos, and organization marks.
- Tournament, conference, or tour pages replace calendar years with day labels, rounds, venues, and participant milestones.
- Editorial features pair archival media with a long chronological narrative and retain a usable mobile layout.
How To Use Lifeline
Prerequisites
Start with a Next.js App Router project that uses Tailwind CSS and has shadcn/ui initialized.
The Lifeline registry item declares lucide-react, next-themes, and geist as package dependencies. It also requests the shadcn utils registry item, which supplies the local cn() helper used throughout the components.
Run the following command if the project does not contain components.json:
npx shadcn@latest initInstall a Complete Timeline Page
The fastest setup adds the component system, personal starter data, page shell, and an App Router route at app/lifeline/page.tsx:
npx shadcn@latest add evilrabbit/lifeline/pageThe CLI asks before replacing an existing route file. Decline the overwrite when app/lifeline/page.tsx already contains work that must remain intact.
Choose a Registry Item
| Registry Item | Command | Result |
|---|---|---|
| Complete page | npx shadcn@latest add evilrabbit/lifeline/page | Adds the shell, personal starter data, components, and app/lifeline/page.tsx. |
| Shell | npx shadcn@latest add evilrabbit/lifeline/shell | Adds the navigation, stage, footer, and core timeline dependency. |
| Personal starter | npx shadcn@latest add evilrabbit/lifeline/personal | Adds the core system and a commented personal timeline data file. |
| Company starter | npx shadcn@latest add evilrabbit/lifeline/company | Adds the core system and company history starter data. |
| Day-based starter | npx shadcn@latest add evilrabbit/lifeline/journey | Adds the core system and a bounded day-by-day data template. |
| Core component system | npx shadcn@latest add evilrabbit/lifeline/lifeline | Adds the timeline components and data helper with no page shell. |
| Theme switcher | npx shadcn@latest add evilrabbit/lifeline/theme-switcher | Adds the demo’s next-themes sun and moon control. |
A hosted registry URL also works:
npx shadcn@latest add https://evilrabbit.com/r/lifeline.jsonBasic Usage
Create timeline data with defineLifeline(), then pass the generated markers and starting year to <Lifeline />.
import { Lifeline, LifelineLegend } from "@/components/lifeline"
import {
LifelineFooter,
LifelineNav,
LifelineShell,
LifelineStage,
} from "@/components/lifeline-shell"
import { defineLifeline } from "@/lib/lifeline-data"
const companyHistory = defineLifeline({
slug: "acme-history",
name: "Acme Product History",
birthYear: 2019,
endYear: 2026,
description: "Major releases and company milestones.",
milestones: {
2019: {
id: "founded",
events: ["Acme opened its first product studio."],
},
2021: {
id: "public-launch",
events: [
{
text: "Version 1.0 launched for public accounts.",
image: {
src: "/timeline/version-one.jpg",
alt: "Acme version 1.0 dashboard",
},
},
],
},
2024: {
id: "mobile-release",
events: ["The mobile application entered general availability."],
photos: [
{
src: "/timeline/mobile-app.jpg",
alt: "Acme mobile application",
width: 200,
},
],
},
2026: {
id: "platform-update",
events: ["The platform added team workspaces and shared reports."],
},
},
})
export default function HistoryPage() {
return (
<LifelineShell>
<LifelineNav
logo={<span className="text-sm font-semibold">ACME</span>}
logoLabel="Acme home"
/>
<LifelineStage>
<Lifeline
markers={companyHistory.markers}
birthYear={companyHistory.birthYear}
title={companyHistory.name}
mode="page"
className="h-full"
/>
</LifelineStage>
<LifelineFooter>
<LifelineLegend />
</LifelineFooter>
</LifelineShell>
)
}Timeline Data Structure
defineLifeline() accepts a record with slug, name, birthYear, description, optional endYear, optional legend labels, and milestones keyed by a numeric axis value. It creates one marker for every value from birthYear through endYear.
| Record Field | Type | Purpose |
|---|---|---|
slug | string | Identifies the timeline record. |
name | string | Supplies the timeline name and common title value. |
birthYear | number | Sets the first axis value and the starting point for computed age labels. |
endYear | number | Sets the final axis value. The helper uses its current-year constant when omitted. |
description | string | Stores a summary with the timeline record. |
legend | LifelineLegendItem[] | Renames the two people categories represented by mentor and met. |
milestones | Record<number, LifelineMilestone> | Maps each year or sequential unit to its event data. |
Milestone Fields
| Field | Accepted Data | Behavior |
|---|---|---|
id | string | Supplies the marker key. |
events | Strings, linked text segments, or event objects | Renders the main event content for the marker. |
label | string | Replaces the raw numeric axis label with text such as Jun 16. |
age | number | string | Overrides the computed age or secondary axis label. |
photos | LifelinePhoto[] | Adds draggable media cards that open in a lightbox. |
badges | { src, alt }[] | Renders small emblems above the event list. |
companies | { id, name }[] | Renders registered organization icons or an initial fallback. |
mentors | { name, role?, color?, photo? }[] | Adds people to the first legend row. |
met | { name, photo? }[] | Adds people to the second legend row. |
The marker, event, media, company, and people shapes come from the installed TypeScript definitions.
An event object accepts text, optional image, and an optional effect:
{
text: "The public beta opened.",
image: {
src: "/timeline/beta.jpg",
alt: "Public beta interface",
video: "/timeline/beta-preview.webm",
},
effect: "fireworks",
}Use linked segments when part of an event needs a destination URL:
events: [
[
{ type: "text", value: "Read the " },
{
type: "link",
value: "release announcement",
href: "/blog/version-two",
},
{ type: "text", value: "." },
],
]Event effects currently accept "fireworks" and "fireworks-argentina".
Position Photo Cards
A photo accepts optional placement values:
photos: [
{
src: "/timeline/team-retreat.jpg",
alt: "Product team retreat",
x: 0.6,
y: 48,
rotate: -3,
width: 220,
},
]x runs from 0 to 1 across the marker’s slot. y uses pixels below the rail, rotate uses degrees, and width uses pixels. The component calculates a scattered position when these values are absent.
Register Company Icons
Register icon components once at module scope. The company id in milestone data must match the registry key.
import { registerCompanyIcons } from "@/components/lifeline/company-icon"
import { AcmeLogo, NorthstarLogo } from "@/components/brand-icons"
registerCompanyIcons({
acme: {
icon: AcmeLogo,
sizeClassName: "h-4 w-5",
},
northstar: {
icon: NorthstarLogo,
sizeClassName: "h-4 w-8",
},
})Unknown company IDs render the first character of the company name inside a small ring.
Lifeline Props
<Lifeline
markers={companyHistory.markers}
birthYear={companyHistory.birthYear}
title={companyHistory.name}
mode="page"
className="h-full"
/>| Prop | Type | Default | Purpose |
|---|---|---|---|
markers | LifelineMarker[] | Required | Supplies the complete sequence of timeline markers. |
birthYear | number | Required | Sets the axis origin and computed age row. |
title | string | "Lifeline" | Sets the accessible label on the timeline region. |
mode | "auto" | "page" | "embed" | "auto" | Selects full-page behavior, embedded behavior, or runtime measurement. |
className | string | None | Extends the desktop root and the embedded mobile scroll box. |
Full-Page and Embedded Modes
Use mode="page" when the timeline owns the page viewport and its scroll input. LifelineShell creates an h-dvh column with clipped overflow, while LifelineStage supplies the scrolling context required by the responsive layouts.
Use mode="embed" for a timeline placed between other sections. Assign a fixed or responsive height to the wrapper:
<section className="h-[720px]">
<Lifeline
markers={companyHistory.markers}
birthYear={companyHistory.birthYear}
title={companyHistory.name}
mode="embed"
className="h-full"
/>
</section>Desktop wheel input moves the rail while the pointer sits over the module. Scroll input returns to the page at the start and end of the timeline. On mobile, embed mode creates a bounded vertical scroll box that chains back to the page at both boundaries.
The wrapper height is mandatory. A missing height collapses the embedded module. Timelines with portrait rows and floating media usually need about 700px to 800px of vertical space.
mode="auto" measures the timeline at runtime. It selects page behavior when the component occupies at least half of the viewport and no outer vertical scroll remains available. Set the mode explicitly when the surrounding layout is already known.
Shell Components
| Component | Main Props | Role |
|---|---|---|
LifelineShell | children, className | Creates the viewport-height page frame. |
LifelineNav | logo, logoHref, logoLabel, children, className, containerClassName | Renders the fixed navigation and adds the rail alignment markers. |
LifelineStage | children, className | Clears the fixed navigation and hosts the responsive scroll area. |
LifelineFooter | children, className, containerClassName | Creates the footer region used for LifelineLegend. |
LifelineNav requires a logo node. Its logoHref defaults to /, and logoLabel defaults to "Home".
Apply the same containerClassName to the navigation and footer. The rail endpoint follows the marked navigation container, while the footer uses its own container class.
Align the Rail with Existing Site Navigation
The supplied shell marks its navigation with two data attributes:
| Attribute | Measurement |
|---|---|
data-site-nav-logo | Sets the left edge of the first marker. |
data-site-nav-inner | Sets the right boundary, minus 24 pixels of internal spacing. |
Add the same attributes to an existing site header when the project already has navigation:
<header>
<div
data-site-nav-inner
className="mx-auto flex h-16 max-w-6xl items-center px-6"
>
<a data-site-nav-logo href="/" aria-label="Home">
<BrandLogo />
</a>
<SiteNavigation />
</div>
</header>The timeline remeasures these elements after resize. A timeline in a narrow card uses its own container when the marked header sits outside the module’s horizontal bounds.
When neither attribute exists, the rail starts 24 pixels inside the stage and ends 24 pixels before its right edge.
Generated Files and Customization
The core registry item writes the component system to:
components/lifeline/
├── company-icon.tsx
├── index.ts
├── lifeline-desktop.tsx
├── lifeline-event.tsx
├── lifeline-fireworks.tsx
├── lifeline-hover-image.tsx
├── lifeline-intro-timing.ts
├── lifeline-labels.tsx
├── lifeline-layout.ts
├── lifeline-legend.tsx
├── lifeline-lightbox.tsx
├── lifeline-marker.tsx
├── lifeline-people.tsx
├── lifeline-photos.tsx
├── lifeline-utils.ts
├── lifeline-vertical.tsx
├── lifeline.tsx
├── types.ts
├── use-lifeline-intro.ts
├── use-lifeline-scroll.ts
└── use-lifeline-vertical-scroll.ts
lib/
└── lifeline-data.tsThe registry also adds the intro keyframes and .lifeline-typeset rules to the project’s CSS. Shell and page registry items add components/lifeline-shell.tsx, starter data, and the optional app/lifeline/page.tsx route.
Change the Typography
Lifeline uses the --lifeline-font custom property. The default stack prefers Geist and falls back to a system sans-serif font.
Load the installed Geist font in the root layout:
import { GeistSans } from "geist/font/sans"
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" className={GeistSans.variable}>
<body>{children}</body>
</html>
)
}Set another font directly in the global stylesheet when the timeline must match an existing design system:
.lifeline-typeset {
--lifeline-font: var(--font-brand-sans), ui-sans-serif, system-ui, sans-serif;
}The registry CSS and component source define the class and custom property.
Change Motion and Layout
The installed source exposes the main tuning values near the top of the relevant files:
lifeline-intro-timing.tsstores rail duration, fade timing, and scaling constants.lifeline-layout.tsstores breakpoint and width calculations.use-lifeline-scroll.tsstores wheel speed, drag speed, momentum, fade zones, and embedded boundary timing.lifeline-hover-image.tsxstores image offset, tilt, and cursor-following values.lifeline-photos.tsxstores card width, overlap, tilt, and stacking values.
Tailwind utility classes control the palette, spacing, borders, shadows, and dark mode. The shipped styles use the zinc palette.
Implementation Notes
Keep the Numeric Axis Current
lib/lifeline-data.ts currently defines LIFELINE_CURRENT_YEAR as 2026. defineLifeline() uses that constant when endYear is absent. Set endYear explicitly for archived timelines, or update the constant when a living timeline must extend into a new year.
Check Image Paths
Local images belong under public when milestone data uses paths such as /timeline/launch.jpg. Add precise images.remotePatterns entries in next.config.ts when installed components render remote media through next/image.
Preserve the Client Boundary
The installed Lifeline component reads window.matchMedia, measures DOM geometry, observes element visibility, and responds to pointer and wheel input. Keep its "use client" directive in place.
Give Mobile Page Mode a Scroll Parent
The vertical layout measures the nearest ancestor whose overflow-y value is auto or scroll. LifelineStage already supplies that structure. A custom page shell needs a bounded ancestor such as min-h-0 flex-1 overflow-y-auto.
Review Existing Files Before Installation
Use the shadcn CLI preview commands when the target project already contains similarly named files:
npx shadcn@latest add evilrabbit/lifeline/page --dry-run
npx shadcn@latest add evilrabbit/lifeline/page --diffThe current add command supports both --dry-run and --diff.
Alternatives and Related Resources
- Minimalist Timeline Component for Next.js: Shadcn Timeline
- Animated Interactive Shadcn Timeline Component for React
- Display Events Interactively with shadcn/ui Event Timeline Components
FAQs
Q: Why does an embedded Lifeline collapse?
A: mode="embed" does not calculate its own height. Add a fixed, viewport-based, or container-derived height to the wrapper and apply h-full to the component.
Q: Why does the desktop rail run from edge to edge?
A: The component did not find data-site-nav-logo and data-site-nav-inner. Add those attributes to the site header or place the timeline inside a capped container.
Q: Why does the timeline stop at 2026?
A: The installed data helper uses LIFELINE_CURRENT_YEAR = 2026 when endYear is missing. Update the constant or pass an explicit endYear.
Q: Can the App Router page remain a Server Component?
A: Yes. The installed Lifeline file defines the client boundary. Keep page-level data and static layout in the Server Component unless the page itself needs hooks or browser APIs.
Q: How do I disable the intro for reduced-motion users?
A: No extra configuration is required. The intro hook checks prefers-reduced-motion: reduce and opens the timeline in its settled state.


