Timeline React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React timeline component for activity feeds, audit trails, changelogs and order tracking, with icons, relative times and day groups.
Read it top to bottom: each event has a marker on the line (a dot, an icon tile in its tone, or the person's avatar), a title, an optional description and a timestamp that stays fresh ("5 min. ago", "yesterday"); hover a timestamp on desktop for the full date and time (use timeFormat "datetime" where people on phones need it). Items with details show a Details button: click or tap it (Enter or Space from the keyboard) and the details slide open while the chevron turns; press again to close. New events slide in at the top and the rest glide down. In the feed variant events sit under day headings ("Today", "Yesterday") that can stick while you scroll. In the tracking variant finished steps get a check, the current step pulses, upcoming ones are hollow, a failed one turns red with an X, and the line fills down to the current step when it scrolls into view. Show older at the end loads more and spins while it waits. Tab only stops on real controls (links in titles, Details, Show older, buttons in meta). On a phone or in a narrow panel the side date column folds under the title and every button has a 44px touch target. Reduced motion drops the stagger, the growth and the pulse.
Timeline is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add timeline, together with Avatar, Button, Empty State, Skeleton, useControllableState and 5 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add timelineTimeline is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Timeline from @/components/ui/timeline.
import { Phone, Receipt } from "lucide-react"
import { Timeline, type TimelineItemData } from "@/components/ui/timeline"
const activity: TimelineItemData[] = [
{
id: "payment",
title: "Invoice #2026-0412 paid",
description: "$12,480.00 by bank transfer",
time: "2026-09-24T16:20:00",
icon: <Receipt />,
tone: "success",
},
{
id: "call",
title: "Call with Adam Foster",
description: "12 min",
time: "2026-09-23T11:05:00",
icon: <Phone />,
actor: { name: "Michael Reed", avatar: "/demo/mihai-ionescu.jpg" },
details: "Asked for training for 2 new teammates.",
},
]
export function ClientActivity() {
return (
<Timeline
items={activity}
locale="en-US"
/>
)
}
// parts, for events the items API does not cover (same file, same look):
// TimelineRoot takes the appearance props of Timeline (variant, size, tone, color, line, timePosition,
// timeFormat, locale, labels, classNames) and holds the shared clock and locale;
// TimelineGroup { label, sticky } is a day heading and its <ol>; without a label it is just the list;
// TimelineItem { status, tone, color, expanded, index } is one <li>; status only applies in the tracking
// variant, where the current step gets aria-current="step";
// TimelineMarker { kind: "dot" | "icon" | "avatar" | "status", icon, actor, children } is the marker and
// the rail down to the next event (hidden on the last one);
// TimelineContent, TimelineTitle { href }, TimelineDescription: the text column, the title (a link with
// href; tracking steps read their status first to screen readers) and the secondary text;
// TimelineTime { value, format, placement: "inline" | "side" } is a <time> with dateTime and a full-date
// title once hydrated; side puts it in the start column on timelines 512px wide and up;
// TimelineDetails { open, defaultOpen, onOpenChange, collapsible } is the Details toggle and its panel;
// DEFAULT_TIMELINE_LABELS holds the English strings to spread a translation over
import { Truck } from "lucide-react"
import {
TimelineRoot, TimelineGroup, TimelineItem, TimelineMarker, TimelineContent, TimelineTitle,
TimelineDescription, TimelineTime, TimelineDetails,
} from "@/components/ui/timeline"
export function Delivery({ tracking, sentAt }: { tracking: string; sentAt: string }) {
return (
<TimelineRoot variant="tracking" locale="en-US" aria-label="Delivery">
<TimelineGroup>
<TimelineItem status="current" index={0}>
<TimelineMarker kind="icon" icon={<Truck />} />
<TimelineContent>
<TimelineTitle href={`/parcels/${tracking}`}>Handed to the courier</TimelineTitle>
<TimelineDescription>Tracking {tracking}</TimelineDescription>
<TimelineTime value={sentAt} />
<TimelineDetails>Estimated delivery tomorrow, between 10 AM and 2 PM.</TimelineDetails>
</TimelineContent>
</TimelineItem>
</TimelineGroup>
</TimelineRoot>
)
}Props
itemsdefault"[]"TimelineItemData[]The events: { id, title, description?, time?: Date | string | number, icon?, tone?, color?, status?: "complete" | "current" | "upcoming" | "error", actor?: { name, avatar? }, meta?, details?, href? }. Items without a time sort last and group under labels.earlier.
variantdefault"default""default" | "feed" | "tracking"default: markers on a line with title, description and time. feed: compact activity rows with avatars and one sentence (bold the names with <b>), grouped by day, no line. tracking: order steps with status markers and a line filled up to the current step.
sizedefault"md""sm" | "md" | "lg"Marker 20 / 28 / 36px (dots 8 / 10 / 12px), title 13 / 14 / 15px, and the spacing between events.
markersdefault"auto""auto" | "dot" | "icon" | "avatar"auto: the icon when an item has one, else the actor's avatar, else a dot (feeds prefer the avatar). The others force a kind and fall back when an item lacks it. Tracking steps always show their status.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The default marker tone: dots, tracking checks and the filled line. Each item can set its own.
colordefault""stringAny CSS color for the markers; replaces the tone. Items with their own tone keep it.
orderdefault"desc""desc" | "asc" | "none"Sorts by time: desc newest first (feeds, logs), asc oldest first (tracking, where it is the default), none keeps your order. Items without a time go last.
groupByDaydefaultfalsebooleanDay headings ("Today", "Yesterday", "Monday", then the date) in the locale. On by default in the feed variant.
stickyGroupsdefaultfalsebooleanDay headings stick to the top of the scroll container. Set --timeline-sticky-top to clear a sticky header and --timeline-bg to the surface color behind them.
timeFormatdefault"relative""relative" | "time" | "date" | "datetime"relative: "5 min. ago", "yesterday", then the date after 4 weeks (under an older day heading it prints the clock time, since the heading names the day). time: 4:40 PM. date: 9/22/2026. datetime: both. Times refresh every minute.
timePositiondefault"inline""inline" | "side"inline: the time at the end of the title row. side: a date column on the start side (changelogs, audit logs) once the timeline itself is 512px wide; narrower (a phone, a side panel) it folds under the title. It is a container query, so it follows the space the timeline gets, not the screen.
linedefaulttruebooleanThe connector between markers. Off by default in the feed variant.
expandabledefaulttruebooleanItems with details get a Details toggle. Off shows every item's details open.
expandedKeysstring[]Ids of the items whose details are open (controlled). Pair with onExpandedKeysChange.
defaultExpandedKeysdefault"[]"string[]Ids open at first while uncontrolled.
onExpandedKeysChange(keys: string[]) => voidFires with the open ids when a Details toggle is pressed.
loadingdefaultfalsebooleanSkeleton events (marker and two lines) in place of the items, with aria-busy.
skeletonCountdefault4numberHow many skeleton events show while loading (1 to 12).
hasMoredefaultfalsebooleanShows a Show older button at the end (needs onLoadMore).
onLoadMore() => (void | Promise<unknown>)Called by Show older. A returned promise keeps the button spinning until it settles.
loadingMoredefaultfalsebooleanSpins the Show older button when you track the request yourself (shown while hasMore is on).
emptyStateReactNodeReplaces the default <EmptyState size="sm" preset="empty" title={labels.empty} /> when there are no items.
errorReactNodeShows the error empty state with this message in place of the items.
onRetry() => (void | Promise<unknown>)Adds Try again to the error state; a returned promise shows it loading.
animateIndefaulttruebooleanEvents rise in one after another when the list appears after the page loaded (after skeletons, on a tab switch), at most 12, and new events slide in at the top. Off, new events simply appear while the rest still glide down. Server-rendered lists show at once.
localedefault""stringLocale for times and day headings, e.g. en-US. Empty uses UIProvider's, then the browser's.
labelsTimelineLabelsInputStrings: showDetails ("Details"), hideDetails ("Hide details"), loadMore ("Show older"), earlier ("Earlier"), empty ("No activity yet"), status { complete, current, upcoming, error } read before tracking titles. English defaults in DEFAULT_TIMELINE_LABELS.
classNamesPartial<Record<"group" | "groupLabel" | "list" | "item" | "rail" | "marker" | "content" | "header" | "title" | "description" | "actor" | "meta" | "time" | "toggle" | "details" | "footer", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root. Set --timeline-bg here when sticky headings sit on a surface.
Parts
TimelineRoot
componentThe outer part of a hand-built timeline. Takes the appearance props of Timeline (variant, size, tone, color, line, timePosition, timeFormat, locale, labels, classNames) and shares the clock and locale with every part inside.
TimelineGroup
componentA day heading and its list. Without a label it renders just the list.
labelReactNodeThe heading, such as Today, Yesterday or Sep 12, 2026.
stickydefaultfalsebooleanThe heading sticks while scrolling (top from --timeline-sticky-top, fill from --timeline-bg).
TimelineItem
componentOne event as a list item. status applies in the tracking variant, where the current step gets aria-current="step".
status"complete" | "current" | "upcoming" | "error"The step state in the tracking variant.
tone"neutral" | "brand" | "success" | "warning" | "danger" | "info"Marker tone for this event; overrides the timeline's.
colorstringAny CSS color for the marker; replaces the tone.
expandedbooleanSets data-expanded on the item, for styling it while its details are open.
indexnumberPosition in the list; delays the tracking line growth.
TimelineMarker
componentThe marker and the rail down to the next event (hidden on the last one).
kind"dot" | "icon" | "avatar" | "status"What the marker shows.
iconReactNodeThe icon for kind icon.
actor{ name: string; avatar?: string }The person for kind avatar.
childrenReactNodeReplaces the marker's content.
TimelineContent
componentThe text column next to the marker: title, description, meta and details.
TimelineTitle
componentThe event title. With href it becomes a link; tracking steps read their status first to screen readers.
TimelineDescription
componentThe secondary text under the title, in the muted color.
TimelineTime
componentA time element with a machine-readable dateTime and a full-date title once hydrated, formatted relative or as a clock time.
value*Date | string | numberThe moment of the event.
formatTimestampFormatOverrides the timeline's timeFormat.
placementdefault"inline""inline" | "side"side puts it in the start column on timelines 512px wide and up.
TimelineDetails
componentThe Details toggle and its collapsible panel under an event.
openbooleanControlled open state.
defaultOpendefaultfalsebooleanStarting state when uncontrolled.
onOpenChange(open: boolean) => voidFires when the toggle opens or closes the panel.
collapsibledefaulttruebooleanfalse shows the content without a toggle.
DEFAULT_TIMELINE_LABELS
constantThe English strings (showDetails, hideDetails, loadMore, earlier, empty and the tracking status words). Spread it to translate. Type: TimelineLabels.
TimelineItemData
typeOne event of the items prop: id, title, description, time, icon, tone or color, status, actor, meta, details and href.
Examples
Activity feed
variant="feed": avatars, bold names, day headings and a Show older button that loads.
Order tracking
variant="tracking": checks, a pulsing current step and a line that fills as it scrolls in.
Changelog
timePosition="side" puts dates in a column when there is room (a container query); details hold the release notes.
Audit log
size="sm", full date and time, a tone per action type.
Failed step
An error status turns the step red; meta carries the recovery action.
Loading and empty
Skeletons in the real row shape, then the shared empty state.
Dependencies
Files
- components/ui/timeline.tsx
FAQ
Is the React Timeline component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Timeline?
Run npx wingo-ui@latest add timeline. It also adds the Wingo UI items it builds on (Avatar, Button, Empty State, Skeleton, useControllableState and 5 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/timeline.
How does Timeline behave on phones?
Read it top to bottom: each event has a marker on the line (a dot, an icon tile in its tone, or the person's avatar), a title, an optional description and a timestamp that stays fresh ("5 min. ago", "yesterday"); hover a timestamp on desktop for the full date and time (use timeFormat "datetime" where people on phones need it).
See also
Used in
More React data display components
Articles using this component
- Audit Log UI in React: Filters, Diffs and CSV Export
Component guides10 min read
- Changelog Page Template in React: RSS, Email, Permalinks
Build it: screens and apps10 min read
- React Gantt Chart: Drag, Progress and Dependencies
Component guides10 min read