Order Tracker
A vertical delivery timeline: completed steps get ticks and times, the current step pulses, and an ETA heads the card.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/order-tracker.json'use client'
import './order-tracker.css'
export interface TrackStep { label: string; time?: string; detail?: string }
export interface OrderTrackerProps {
steps: TrackStep[]
current: number
eta?: string
fill?: 'dark' | 'light'
className?: string
}
/** Vertical delivery timeline. */
export function OrderTracker({ steps, current, eta, fill = 'dark', className }: OrderTrackerProps) {
return (
<div className={['sd-otr', className].filter(Boolean).join(' ')} data-fill={fill}>
{eta && <div className="sd-otr__eta">{current >= steps.length - 1 ? 'Delivered' : 'Arriving'}<b>{eta}</b></div>}
<ol>
{steps.map((s, i) => {
const st = i < current || (i === current && i === steps.length - 1) ? 'done' : i === current ? 'now' : 'todo'
return (
<li key={s.label} className="sd-otr__s" data-st={st} aria-current={st === 'now' ? 'step' : undefined} aria-label={`${s.label}${st === 'done' ? ', completed' : st === 'now' ? ', in progress' : ''}${s.time && st !== 'todo' ? `, ${s.time}` : ''}`}>
<span className="sd-otr__dot" aria-hidden>{st === 'done' && <svg viewBox="0 0 12 12"><path d="m2.5 6.5 2.5 2.5 4.5-5.5" /></svg>}</span>
<div aria-hidden>
<div className="sd-otr__l">{s.label}{s.time && st !== 'todo' && <span>{s.time}</span>}</div>
{s.detail && st !== 'todo' && <div className="sd-otr__d">{s.detail}</div>}
</div>
</li>
)
})}
</ol>
</div>
)
}.sd-otr { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-otr[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-otr button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-otr :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-otr .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-otr { width: min(100%, 320px); padding: 16px; border-radius: 22px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); }
.sd-otr__eta { font-size: 12.5px; color: var(--mute); }
.sd-otr__eta b { display: block; color: var(--fg); font-size: 20px; font-weight: 650; letter-spacing: -0.01em; margin-top: 2px; }
.sd-otr ol { list-style: none; margin: 16px 0 0; padding: 0; }
.sd-otr__s { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 12px; padding-bottom: 20px; }
.sd-otr__s:last-child { padding-bottom: 0; }
.sd-otr__s:not(:last-child)::before, .sd-otr__s:not(:last-child)::after { content: ''; position: absolute; left: 11px; top: 24px; bottom: 0; width: 2px; background: var(--soft); }
.sd-otr__s:not(:last-child)::after { background: var(--fg); transform-origin: top; transform: scaleY(0); transition: transform 500ms ease; }
.sd-otr__s[data-st='done']::after { transform: scaleY(1); }
.sd-otr__dot { position: relative; z-index: 1; display: grid; place-items: center; width: 24px; height: 24px; border-radius: 99px; background: var(--bg); box-shadow: inset 0 0 0 2px var(--line); transition: background-color 300ms, box-shadow 300ms; }
.sd-otr__s[data-st='done'] .sd-otr__dot { background: var(--strong); box-shadow: none; color: var(--on); }
.sd-otr__s[data-st='now'] .sd-otr__dot { box-shadow: inset 0 0 0 2px var(--fg); }
.sd-otr__s[data-st='now'] .sd-otr__dot::before { content: ''; width: 8px; height: 8px; border-radius: 99px; background: var(--fg); }
.sd-otr__s[data-st='now'] .sd-otr__dot::after { content: ''; position: absolute; inset: -5px; border-radius: 99px; box-shadow: inset 0 0 0 1.5px var(--fg); animation: sd-otr-ping 1.8s ease-out infinite; }
@keyframes sd-otr-ping { from { transform: scale(0.7); opacity: 0.8; } to { transform: scale(1.25); opacity: 0; } }
.sd-otr__dot svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.sd-otr__l { font-size: 14px; font-weight: 600; display: flex; justify-content: space-between; gap: 8px; }
.sd-otr__s[data-st='todo'] .sd-otr__l { color: var(--mute); font-weight: 500; }
.sd-otr__l span { font-size: 12px; font-weight: 400; color: var(--mute); }
.sd-otr__d { font-size: 12.5px; color: var(--mute); margin-top: 2px; }
@media (prefers-reduced-motion: reduce) { .sd-otr *, .sd-otr *::before, .sd-otr *::after { animation: none !important; transition: none !important; } }Overview
Ordered, Shipped, Out for delivery, Delivered. The connecting line fills down to the current step, which has a slow pulsing ring so it reads as live.
Each step can carry a timestamp and a detail line such as the carrier or the place it was handed over.
When to use
- Order pages, delivery apps, shipment emails turned into pages.
When not to use
- Processes without a fixed sequence.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/order-tracker.json$
npx shadcn@latest add https://sundaeui.dev/r/order-tracker.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/order-tracker.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/order-tracker.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy order-tracker.tsx and order-tracker.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { OrderTracker } from '@/components/sundae/order-tracker'
<OrderTracker />Props
| Prop | Type | Default |
|---|---|---|
steps*Steps in order. | { label: string; time?: string; detail?: string }[] | — |
current*Index of the current step. | number | — |
etaEstimated arrival text. | string | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- An ordered list; the current step has aria-current="step" and completed steps say so in their label.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-otr; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Order Tracker free?
- Yes. Order Tracker is free for personal and commercial projects, with full source on this page.
- How do I install Order Tracker?
- Run npx shadcn@latest add https://sundaeui.dev/r/order-tracker.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Order Tracker support keyboard and screen readers?
- An ordered list; the current step has aria-current="step" and completed steps say so in their label. Reduced motion removes the movement and keeps every state change.
- Does Order Tracker respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →Add to Cart
Components · RefinedVideo Trimmer
Components · RefinedCI Checks
Components · RefinedRide Tracker
Components · RefinedFlight Status
Components · RefinedIncident Timeline
Components · Refined