Commerce
Order status
A shipment tracker: delivery estimate, a track of dated steps, any delay, and the tracking number.
- Built on
<ol>aria-current="step"<time>Intl.DateTimeFormatContainer queries- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 4.4 kB
- Built from
- Icon
Arrives
- Ordered, completed
- Packed, completed
- Shipped, current
- Delivered, upcoming
Try its options
Loading its options.
Code
import { OrderStatus } from "@/components/equip/order-status";
export default function Demo() {
return (
<OrderStatus
size="s"
orientation="horizontal"
estimate="2026-11-20"
current={2}
steps={[
{ label: "Ordered", date: "2026-11-12" },
{ label: "Packed", date: "2026-11-13" },
{ label: "Shipped", date: "2026-11-14" },
{ label: "Delivered" },
]}
style={{ width: 224 }}
/>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:224px" class="eq-order-status" data-orientation="horizontal" data-size="s">
<div class="eq-order-status-header">
<p class="eq-order-status-estimate">Arrives <time dateTime="2026-11-20">Friday, November 20</time></p>
</div>
<ol class="eq-order-status-track" aria-label="Shipment progress">
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="10" style="--_size:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Ordered<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-12">Nov 12</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="10" style="--_size:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Packed<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-13">Nov 13</time>
</span>
</li>
<li class="eq-order-status-step" data-state="current" aria-current="step">
<span class="eq-order-status-line" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="10" style="--_size:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Shipped<span class="eq-order-status-hidden">, current</span></span>
<time class="eq-order-status-date" dateTime="2026-11-14">Nov 14</time>
</span>
</li>
<li class="eq-order-status-step" data-state="upcoming">
<span class="eq-order-status-marker" aria-hidden="true"></span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Delivered<span class="eq-order-status-hidden">, upcoming</span></span>
</span>
</li>
</ol>
</div>Examples
Tracker with carrier
Below about 30rem of container width the track turns into a vertical list, so it fits a phone or a sidebar.
Ceramic pour-over set
Order #48213
Arrives –
- Ordered, completed
- Packed, completed
- Shipped, currentLeft the Reno warehouse
- Out for delivery, upcoming
- Delivered, upcoming
UPS GroundTracking number 1Z 999 AA1 01 2345 6784
Track packageCode
import { OrderStatus } from "@/components/equip/order-status";
export default function Example() {
return (
<OrderStatus
heading="Ceramic pour-over set"
orderNumber="#48213"
estimate={{ from: "2026-11-19", to: "2026-11-21" }}
current={2}
steps={[
{ label: "Ordered", date: "2026-11-12" },
{ label: "Packed", date: "2026-11-13" },
{ label: "Shipped", date: "2026-11-14", description: "Left the Reno warehouse" },
{ label: "Out for delivery" },
{ label: "Delivered" },
]}
carrier="UPS Ground"
trackingNumber="1Z 999 AA1 01 2345 6784"
trackingHref="https://www.ups.com/track"
/>
);
}HTMLworks in any framework, with the part's CSS
<div class="eq-order-status" data-orientation="auto" data-size="m">
<div class="eq-order-status-header">
<div class="eq-order-status-title">
<h3 class="eq-order-status-heading">Ceramic pour-over set</h3>
<p class="eq-order-status-order">Order <span class="eq-order-status-mono">#48213</span></p>
</div>
<p class="eq-order-status-estimate">Arrives <time dateTime="2026-11-19">Thursday, November 19</time><span> – </span><time dateTime="2026-11-21">Saturday, November 21</time></p>
</div>
<ol class="eq-order-status-track" aria-label="Shipment progress">
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Ordered<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-12">Nov 12</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Packed<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-13">Nov 13</time>
</span>
</li>
<li class="eq-order-status-step" data-state="current" aria-current="step">
<span class="eq-order-status-line" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Shipped<span class="eq-order-status-hidden">, current</span></span>
<time class="eq-order-status-date" dateTime="2026-11-14">Nov 14</time>
<span class="eq-order-status-description">Left the Reno warehouse</span>
</span>
</li>
<li class="eq-order-status-step" data-state="upcoming">
<span class="eq-order-status-line" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true"></span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Out for delivery<span class="eq-order-status-hidden">, upcoming</span></span>
</span>
</li>
<li class="eq-order-status-step" data-state="upcoming">
<span class="eq-order-status-marker" aria-hidden="true"></span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Delivered<span class="eq-order-status-hidden">, upcoming</span></span>
</span>
</li>
</ol>
<div class="eq-order-status-tracking">
<p class="eq-order-status-carrier">
<span class="eq-order-status-carrier-name">UPS Ground</span>
<span><span class="eq-order-status-hidden">Tracking number </span><span class="eq-order-status-mono">1Z 999 AA1 01 2345 6784</span></span>
</p>
<a class="eq-order-status-link" href="https://www.ups.com/track">Track package<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="14" style="--_size:14px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M19.5 13.5v5a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h5" /><path d="M14.5 3.5h6v6" /><path d="M20.5 3.5l-9 9" /></svg></a>
</div>
</div>Delayed shipment
The held step says why in words and with an icon, and assistive tech hears it as delayed rather than current.
Merino crew socks, 3 pairs
Order #48177
Arrives
- Ordered, completed
- Packed, completed
- Shipped, completed
- In transit, Delayed
- Delivered, upcoming
Delayed Weather in the Denver hub has held parcels for a day. Your new delivery date is Monday.
FedEx Home DeliveryTracking number 7712 6045 3381
Track packageCode
import { OrderStatus } from "@/components/equip/order-status";
export default function Example() {
return (
<OrderStatus
heading="Merino crew socks, 3 pairs"
orderNumber="#48177"
estimate="2026-11-23"
current={3}
exception={{ message: "Weather in the Denver hub has held parcels for a day. Your new delivery date is Monday." }}
steps={[
{ label: "Ordered", date: "2026-11-10" },
{ label: "Packed", date: "2026-11-11" },
{ label: "Shipped", date: "2026-11-12" },
{ label: "In transit", date: "2026-11-14" },
{ label: "Delivered" },
]}
carrier="FedEx Home Delivery"
trackingNumber="7712 6045 3381"
trackingHref="https://www.fedex.com/fedextrack"
/>
);
}HTMLworks in any framework, with the part's CSS
<div class="eq-order-status" data-orientation="auto" data-size="m" data-tone="warn">
<div class="eq-order-status-header">
<div class="eq-order-status-title">
<h3 class="eq-order-status-heading">Merino crew socks, 3 pairs</h3>
<p class="eq-order-status-order">Order <span class="eq-order-status-mono">#48177</span></p>
</div>
<p class="eq-order-status-estimate">Arrives <time dateTime="2026-11-23">Monday, November 23</time></p>
</div>
<ol class="eq-order-status-track" aria-label="Shipment progress">
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Ordered<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-10">Nov 10</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Packed<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-11">Nov 11</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Shipped<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-12">Nov 12</time>
</span>
</li>
<li class="eq-order-status-step" data-state="current" data-tone="warn" aria-current="step">
<span class="eq-order-status-line" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 4 21 19.5H3z" />
<path d="M12 9.5v3.5M12 16.75v.01" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">In transit<span class="eq-order-status-hidden">, Delayed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-14">Nov 14</time>
</span>
</li>
<li class="eq-order-status-step" data-state="upcoming">
<span class="eq-order-status-marker" aria-hidden="true"></span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Delivered<span class="eq-order-status-hidden">, upcoming</span></span>
</span>
</li>
</ol>
<div class="eq-order-status-exception" data-tone="warn">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 7v5l3.5 2.5" />
</svg>
<p class="eq-order-status-exception-text"><strong class="eq-order-status-exception-title">Delayed</strong> Weather in the Denver hub has held parcels for a day. Your new delivery date is Monday.</p>
</div>
<div class="eq-order-status-tracking">
<p class="eq-order-status-carrier">
<span class="eq-order-status-carrier-name">FedEx Home Delivery</span>
<span><span class="eq-order-status-hidden">Tracking number </span><span class="eq-order-status-mono">7712 6045 3381</span></span>
</p>
<a class="eq-order-status-link" href="https://www.fedex.com/fedextrack">Track package<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="14" style="--_size:14px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M19.5 13.5v5a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h5" /><path d="M14.5 3.5h6v6" /><path d="M20.5 3.5l-9 9" /></svg></a>
</div>
</div>Delivered, in a narrow column
Once the last step is reached the estimate gives way to the delivery date.
Walnut desk lamp
Order #47930
Delivered
- Ordered, completed
- Shipped, completed
- Out for delivery, completed
- Delivered, completedLeft with the building concierge
USPS Priority MailTracking number 9400 1118 9922 3456 7890 12
Code
import { OrderStatus } from "@/components/equip/order-status";
export default function Example() {
return (
<OrderStatus
style={{ maxWidth: 320 }}
heading="Walnut desk lamp"
orderNumber="#47930"
estimate="2026-11-18"
current={3}
steps={[
{ label: "Ordered", date: "2026-11-09" },
{ label: "Shipped", date: "2026-11-11" },
{ label: "Out for delivery", date: "2026-11-13" },
{ label: "Delivered", date: "2026-11-13", description: "Left with the building concierge" },
]}
carrier="USPS Priority Mail"
trackingNumber="9400 1118 9922 3456 7890 12"
/>
);
}HTMLworks in any framework, with the part's CSS
<div style="max-width:320px" class="eq-order-status" data-orientation="auto" data-size="m">
<div class="eq-order-status-header">
<div class="eq-order-status-title">
<h3 class="eq-order-status-heading">Walnut desk lamp</h3>
<p class="eq-order-status-order">Order <span class="eq-order-status-mono">#47930</span></p>
</div>
<p class="eq-order-status-estimate" data-delivered="true">Delivered <time dateTime="2026-11-13">Friday, November 13</time></p>
</div>
<ol class="eq-order-status-track" aria-label="Shipment progress">
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Ordered<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-09">Nov 9</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Shipped<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-11">Nov 11</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Out for delivery<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-13">Nov 13</time>
</span>
</li>
<li class="eq-order-status-step" data-state="completed" aria-current="step">
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Delivered<span class="eq-order-status-hidden">, completed</span></span>
<time class="eq-order-status-date" dateTime="2026-11-13">Nov 13</time>
<span class="eq-order-status-description">Left with the building concierge</span>
</span>
</li>
</ol>
<div class="eq-order-status-tracking">
<p class="eq-order-status-carrier">
<span class="eq-order-status-carrier-name">USPS Priority Mail</span>
<span><span class="eq-order-status-hidden">Tracking number </span><span class="eq-order-status-mono">9400 1118 9922 3456 7890 12</span></span>
</p>
</div>
</div>French
Dates come from Intl in the given locale; labels replaces the built-in words.
Sac à dos en toile cirée
Commande FR-20931
Livraison prévue – novembre
- Commandé, terminé
- Préparé, en cours
- Expédié, à venir
- Livré, à venir
ColissimoNuméro de suivi 6A12345678901
Suivre le colisCode
import { OrderStatus } from "@/components/equip/order-status";
export default function Example() {
return (
<OrderStatus
locale="fr-FR"
heading="Sac à dos en toile cirée"
orderNumber="FR-20931"
estimate={{ from: "2026-11-18", to: "2026-11-19" }}
current={1}
steps={[
{ label: "Commandé", date: "2026-11-13" },
{ label: "Préparé", date: "2026-11-14" },
{ label: "Expédié" },
{ label: "Livré" },
]}
carrier="Colissimo"
trackingNumber="6A12345678901"
trackingHref="https://www.laposte.fr/outils/suivre-vos-envois"
labels={{
progress: "Suivi de l'envoi",
arrives: "Livraison prévue",
delivered: "Livré",
order: "Commande",
tracking: "Numéro de suivi",
track: "Suivre le colis",
completed: "terminé",
current: "en cours",
upcoming: "à venir",
delayed: "Retardé",
problem: "Problème de livraison",
}}
/>
);
}HTMLworks in any framework, with the part's CSS
<div class="eq-order-status" data-orientation="auto" data-size="m">
<div class="eq-order-status-header">
<div class="eq-order-status-title">
<h3 class="eq-order-status-heading">Sac à dos en toile cirée</h3>
<p class="eq-order-status-order">Commande <span class="eq-order-status-mono">FR-20931</span></p>
</div>
<p class="eq-order-status-estimate">Livraison prévue <time dateTime="2026-11-18">mercredi 18</time><span> – </span><time dateTime="2026-11-19">jeudi 19</time><span> novembre</span></p>
</div>
<ol class="eq-order-status-track" aria-label="Suivi de l'envoi">
<li class="eq-order-status-step" data-state="completed">
<span class="eq-order-status-line" data-filled="true" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Commandé<span class="eq-order-status-hidden">, terminé</span></span>
<time class="eq-order-status-date" dateTime="2026-11-13">13 nov.</time>
</span>
</li>
<li class="eq-order-status-step" data-state="current" aria-current="step">
<span class="eq-order-status-line" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="12" style="--_size:12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.5 12.5l5 5 10-11" />
</svg>
</span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Préparé<span class="eq-order-status-hidden">, en cours</span></span>
<time class="eq-order-status-date" dateTime="2026-11-14">14 nov.</time>
</span>
</li>
<li class="eq-order-status-step" data-state="upcoming">
<span class="eq-order-status-line" aria-hidden="true"></span>
<span class="eq-order-status-marker" aria-hidden="true"></span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Expédié<span class="eq-order-status-hidden">, à venir</span></span>
</span>
</li>
<li class="eq-order-status-step" data-state="upcoming">
<span class="eq-order-status-marker" aria-hidden="true"></span>
<span class="eq-order-status-text">
<span class="eq-order-status-label">Livré<span class="eq-order-status-hidden">, à venir</span></span>
</span>
</li>
</ol>
<div class="eq-order-status-tracking">
<p class="eq-order-status-carrier">
<span class="eq-order-status-carrier-name">Colissimo</span>
<span><span class="eq-order-status-hidden">Numéro de suivi </span><span class="eq-order-status-mono">6A12345678901</span></span>
</p>
<a class="eq-order-status-link" href="https://www.laposte.fr/outils/suivre-vos-envois">Suivre le colis<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="14" style="--_size:14px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M19.5 13.5v5a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2h5" /><path d="M14.5 3.5h6v6" /><path d="M20.5 3.5l-9 9" /></svg></a>
</div>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
steps | { label: ReactNode; date?: string; description?: ReactNode }[] | none | The stages in order. date is an ISO date. |
current | number | none | Index of the latest step reached. Earlier steps are completed. |
estimate | string | { from: string; to?: string } | none | Expected delivery as an ISO date or a range. Replaced by the delivery date once the last step is reached. |
carrier | ReactNode | none | Carrier and service, such as UPS Ground. |
trackingNumber | string | none | Shown in monospace. |
trackingHref | string | none | The carrier's tracking page; adds a Track package link. |
exception | { message: ReactNode; tone?: "warn" | "danger"; title?: ReactNode } | none | Marks the current step as held up and shows the reason. warn reads Delayed, danger Delivery problem. |
orderNumber | string | none | Shown beside the heading. |
heading | ReactNode | none | Usually the product or order name. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Level of the heading element. |
locale | string | "en-US" | Formats every date, in UTC. |
orientation | "auto" | "horizontal" | "vertical" | "auto" | auto is a vertical list until the container is 30rem wide. |
size | "s" | "m" | "m" | s has smaller markers and text, for cards and summaries. |
labels | Partial<OrderStatusLabels> | none | Replaces the built-in words, such as Arrives and completed, for another language. |
...rest | ComponentProps<"div"> | none | Every native div attribute, applied to the root. |
Anatomy
| root | .eq-order-status | The container the layout queries. |
|---|---|---|
| header | .eq-order-status-header | |
| title | .eq-order-status-title | Holds the heading and order number. |
| heading | .eq-order-status-heading | |
| order | .eq-order-status-order | |
| estimate | .eq-order-status-estimate | Arrives or Delivered, with the date. |
| mono | .eq-order-status-mono | Order and tracking numbers. |
| track | .eq-order-status-track | The ordered list of steps. |
| step | .eq-order-status-step | data-state is completed, current or upcoming; data-tone marks a held step. |
| line | .eq-order-status-line | Joins a step to the next and fills once passed. |
| marker | .eq-order-status-marker | |
| text | .eq-order-status-text | |
| label | .eq-order-status-label | |
| date | .eq-order-status-date | |
| description | .eq-order-status-description | |
| exception | .eq-order-status-exception | |
| exception text | .eq-order-status-exception-text | |
| exception title | .eq-order-status-exception-title | |
| tracking | .eq-order-status-tracking | |
| carrier | .eq-order-status-carrier | |
| carrier name | .eq-order-status-carrier-name | |
| link | .eq-order-status-link | |
| hidden | .eq-order-status-hidden | Text for assistive tech only, such as each step's state. |
Notes
- A status display, not navigation: steps are plain list items and the only link is the carrier's tracking page.
- Each step's state is spoken after its label, so the track reads as Shipped, current rather than by colour.
- A held step says Delayed or Delivery problem in words and with an icon as well as its tone.
- Dates are formatted in UTC, so the server and the browser print the same day.
- Renders on the server with no client JavaScript.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/order-status.tsx245 lines
import type { ComponentProps, ReactNode } from "react";
import { Icon } from "./icon";
import "./order-status.css";
export type OrderStatusStep = {
label: ReactNode;
/** ISO date the step was reached, or is expected. */
date?: string;
description?: ReactNode;
};
export type OrderStatusLabels = {
/** Names the list of steps. */
progress: string;
arrives: string;
delivered: string;
order: string;
tracking: string;
track: string;
completed: string;
current: string;
upcoming: string;
delayed: string;
problem: string;
};
const DEFAULT_LABELS: OrderStatusLabels = {
progress: "Shipment progress",
arrives: "Arrives",
delivered: "Delivered",
order: "Order",
tracking: "Tracking number",
track: "Track package",
completed: "completed",
current: "current",
upcoming: "upcoming",
delayed: "Delayed",
problem: "Delivery problem",
};
export type OrderStatusProps = Omit<ComponentProps<"div">, "children"> & {
steps: OrderStatusStep[];
/** Index of the latest step reached. Steps before it are completed. */
current: number;
/** Expected delivery: one ISO date, or a range. Replaced by the delivery date once the last step is reached. */
estimate?: string | { from: string; to?: string };
carrier?: ReactNode;
trackingNumber?: string;
/** The carrier's tracking page. */
trackingHref?: string;
/** Marks the current step as held up, with the reason in words. */
exception?: { message: ReactNode; tone?: "warn" | "danger"; title?: ReactNode };
orderNumber?: string;
heading?: ReactNode;
headingLevel?: 2 | 3 | 4 | 5 | 6;
/** Formats every date. */
locale?: string;
/** Horizontal track, vertical list, or horizontal only when the container is wide enough. */
orientation?: "auto" | "horizontal" | "vertical";
size?: "s" | "m";
/** Replaces the built-in words, for another language. */
labels?: Partial<OrderStatusLabels>;
};
/** A bare date is read as midnight UTC, and every date is shown in UTC, so server and browser agree. */
function toDate(iso: string) {
return new Date(/^\d{4}-\d{2}-\d{2}$/.test(iso) ? `${iso}T00:00:00Z` : iso);
}
export function OrderStatus({
steps,
current,
estimate,
carrier,
trackingNumber,
trackingHref,
exception,
orderNumber,
heading,
headingLevel = 3,
locale = "en-US",
orientation = "auto",
size = "m",
labels: labelOverrides,
className,
...rest
}: OrderStatusProps) {
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
const last = steps.length - 1;
const at = Math.max(0, Math.min(current, last));
const delivered = at === last && !exception;
const tone = exception ? (exception.tone ?? "warn") : undefined;
const short = new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", timeZone: "UTC" });
const long = new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", timeZone: "UTC" });
let headline: ReactNode = null;
const deliveredOn = steps[last]?.date;
if (delivered) {
headline = (
<>
{labels.delivered}
{deliveredOn && (
<>
{" "}
<time dateTime={deliveredOn}>{long.format(toDate(deliveredOn))}</time>
</>
)}
</>
);
} else if (estimate) {
const from = typeof estimate === "string" ? estimate : estimate.from;
const to = typeof estimate === "string" ? undefined : estimate.to;
headline = (
<>
{labels.arrives}{" "}
{to ? (
// Each end of the range in its own time element; shared words such as the month sit between.
long.formatRangeToParts(toDate(from), toDate(to)).reduce<{ source: string; text: string }[]>((runs, part) => {
const prev = runs[runs.length - 1];
if (prev && prev.source === part.source) prev.text += part.value;
else runs.push({ source: part.source, text: part.value });
return runs;
}, []).map((run, i) =>
run.source === "shared" ? (
<span key={i}>{run.text}</span>
) : (
<time key={i} dateTime={run.source === "startRange" ? from : to}>
{run.text}
</time>
),
)
) : (
<time dateTime={from}>{long.format(toDate(from))}</time>
)}
</>
);
}
const glyph = size === "s" ? 10 : 12;
const Heading = `h${headingLevel}` as const;
const hasHeader = heading != null || orderNumber != null || headline != null;
const hasTracking = carrier != null || trackingNumber != null || trackingHref != null;
return (
<div
{...rest}
className={["eq-order-status", className].filter(Boolean).join(" ")}
data-orientation={orientation}
data-size={size}
data-tone={tone}
>
{hasHeader && (
<div className="eq-order-status-header">
{(heading != null || orderNumber != null) && (
<div className="eq-order-status-title">
{heading != null && <Heading className="eq-order-status-heading">{heading}</Heading>}
{orderNumber != null && (
<p className="eq-order-status-order">
{labels.order} <span className="eq-order-status-mono">{orderNumber}</span>
</p>
)}
</div>
)}
{headline != null && (
<p className="eq-order-status-estimate" data-delivered={delivered || undefined}>
{headline}
</p>
)}
</div>
)}
<ol className="eq-order-status-track" aria-label={labels.progress}>
{steps.map((step, index) => {
const state = index < at || (index === at && delivered) ? "completed" : index === at ? "current" : "upcoming";
const held = index === at && tone !== undefined;
return (
<li
key={index}
className="eq-order-status-step"
data-state={state}
data-tone={held ? tone : undefined}
aria-current={index === at ? "step" : undefined}
>
{index < last && <span className="eq-order-status-line" data-filled={index < at || undefined} aria-hidden="true" />}
<span className="eq-order-status-marker" aria-hidden="true">
{held ? (
<Icon name="warning" size={glyph} />
) : state === "completed" || state === "current" ? (
<Icon name="check" size={glyph} />
) : null}
</span>
<span className="eq-order-status-text">
<span className="eq-order-status-label">
{step.label}
<span className="eq-order-status-hidden">
, {held ? (tone === "danger" ? labels.problem : labels.delayed) : labels[state]}
</span>
</span>
{step.date && (
<time className="eq-order-status-date" dateTime={step.date}>
{short.format(toDate(step.date))}
</time>
)}
{step.description != null && <span className="eq-order-status-description">{step.description}</span>}
</span>
</li>
);
})}
</ol>
{exception && (
<div className="eq-order-status-exception" data-tone={tone}>
<Icon name={tone === "danger" ? "warning" : "clock"} size="s" />
<p className="eq-order-status-exception-text">
<strong className="eq-order-status-exception-title">
{exception.title ?? (tone === "danger" ? labels.problem : labels.delayed)}
</strong>{" "}
{exception.message}
</p>
</div>
)}
{hasTracking && (
<div className="eq-order-status-tracking">
<p className="eq-order-status-carrier">
{carrier != null && <span className="eq-order-status-carrier-name">{carrier}</span>}
{trackingNumber != null && (
<span>
<span className="eq-order-status-hidden">{labels.tracking} </span>
<span className="eq-order-status-mono">{trackingNumber}</span>
</span>
)}
</p>
{trackingHref && (
<a className="eq-order-status-link" href={trackingHref}>
{labels.track}
<Icon name="external" size={14} />
</a>
)}
</div>
)}
</div>
);
}components/equip/order-status.css371 lines
@layer equip {
.eq-order-status {
--_m: 20px;
--_tone: var(--eq-warn);
container-type: inline-size;
box-sizing: border-box;
inline-size: 100%;
display: grid;
gap: var(--eq-space-4);
min-width: 0;
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-s) / 1.4 var(--eq-font);
}
.eq-order-status[data-size="s"] {
--_m: 16px;
gap: var(--eq-space-3);
}
.eq-order-status[data-tone="danger"],
.eq-order-status-step[data-tone="danger"],
.eq-order-status-exception[data-tone="danger"] {
--_tone: var(--eq-danger);
}
/* ---- header ---- */
.eq-order-status-header {
display: grid;
gap: var(--eq-space-1);
}
.eq-order-status-title {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: var(--eq-space-1) var(--eq-space-3);
}
.eq-order-status-heading {
margin: 0;
font: var(--eq-weight-strong) var(--eq-text-m) / 1.3 var(--eq-font);
}
.eq-order-status-order {
margin: 0;
color: var(--eq-ink-2);
}
.eq-order-status-estimate {
margin: 0;
font: var(--eq-weight-strong) var(--eq-text-l) / 1.3 var(--eq-font);
letter-spacing: -0.01em;
text-wrap: balance;
}
.eq-order-status[data-size="s"] .eq-order-status-estimate {
font-size: var(--eq-text-m);
letter-spacing: 0;
}
.eq-order-status-mono {
font-family: var(--eq-font-mono);
font-variation-settings: var(--eq-font-mono-settings);
font-size: 0.95em;
overflow-wrap: anywhere;
}
/* ---- track: a vertical list by default ---- */
.eq-order-status-track {
display: grid;
margin: 0;
padding: 0;
list-style: none;
}
.eq-order-status-step {
position: relative;
display: grid;
grid-template-columns: var(--_m) minmax(0, 1fr);
column-gap: var(--eq-space-3);
min-width: 0;
padding-bottom: var(--eq-space-4);
}
.eq-order-status-step:last-child {
padding-bottom: 0;
}
.eq-order-status-line {
position: absolute;
left: calc(var(--_m) / 2 - 1px);
top: calc(var(--_m) + var(--eq-space-1));
bottom: var(--eq-space-1);
width: 2px;
border-radius: var(--eq-radius-full);
background: var(--eq-line-strong);
overflow: hidden;
}
.eq-order-status-line::after {
content: "";
position: absolute;
inset: 0;
background: var(--eq-solid);
scale: 1 0;
transform-origin: center top;
transition: scale var(--eq-dur-move) var(--eq-spring);
}
.eq-order-status-line[data-filled]::after {
scale: 1;
}
.eq-order-status-marker {
box-sizing: border-box;
display: grid;
place-items: center;
width: var(--_m);
height: var(--_m);
border: 1px solid var(--eq-line-strong);
border-radius: var(--eq-radius-full);
background: var(--eq-raised);
color: var(--eq-solid-ink);
transition:
background-color var(--eq-dur) var(--eq-ease),
border-color var(--eq-dur) var(--eq-ease),
box-shadow var(--eq-dur) var(--eq-ease);
}
.eq-order-status-step:is([data-state="completed"], [data-state="current"]) .eq-order-status-marker {
border-color: var(--eq-solid);
background: var(--eq-solid);
}
/* The latest step reached wears a halo, so it reads as where the parcel is now. */
.eq-order-status-step[data-state="current"] .eq-order-status-marker {
box-shadow: 0 0 0 3px var(--eq-tint-strong);
}
.eq-order-status-step[data-tone] .eq-order-status-marker {
border-color: var(--_tone);
background: var(--_tone);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--_tone) 22%, transparent);
}
.eq-order-status-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
/* Sets the label's first line on the marker's centre. */
padding-top: calc((var(--_m) - 1.4em) / 2);
}
.eq-order-status-label {
font-weight: var(--eq-weight-strong);
overflow-wrap: break-word;
}
.eq-order-status-step[data-state="upcoming"] .eq-order-status-label {
color: var(--eq-ink-2);
font-weight: var(--eq-weight);
}
.eq-order-status-step[data-tone] .eq-order-status-label {
color: var(--_tone);
}
.eq-order-status-date {
color: var(--eq-ink-2);
font-size: var(--eq-text-xs);
font-variant-numeric: tabular-nums;
}
.eq-order-status-description {
color: var(--eq-ink-2);
}
/* ---- track: horizontal, when asked or when the container is wide ---- */
.eq-order-status[data-orientation="horizontal"] .eq-order-status-track {
display: flex;
}
.eq-order-status[data-orientation="horizontal"] .eq-order-status-step {
flex: 1 1 0;
display: flex;
flex-direction: column;
gap: var(--eq-space-2);
padding: 0 var(--eq-space-2) 0 0;
}
.eq-order-status[data-orientation="horizontal"] .eq-order-status-step:last-child {
padding-inline-end: 0;
}
.eq-order-status[data-orientation="horizontal"] .eq-order-status-line {
top: calc(var(--_m) / 2 - 1px);
bottom: auto;
left: calc(var(--_m) + var(--eq-space-1));
right: var(--eq-space-1);
width: auto;
height: 2px;
}
.eq-order-status[data-orientation="horizontal"] .eq-order-status-line::after {
scale: 0 1;
transform-origin: left center;
}
.eq-order-status[data-orientation="horizontal"] .eq-order-status-line[data-filled]::after {
scale: 1;
}
.eq-order-status[data-orientation="horizontal"] .eq-order-status-text {
padding-top: 0;
}
.eq-order-status[data-orientation="horizontal"][data-size="s"] .eq-order-status-step {
padding-inline-end: var(--eq-space-1);
}
.eq-order-status[data-orientation="horizontal"][data-size="s"] .eq-order-status-step:last-child {
padding-inline-end: 0;
}
.eq-order-status[data-orientation="horizontal"][data-size="s"] .eq-order-status-label {
font-size: var(--eq-text-xs);
}
@container (width >= 30rem) {
.eq-order-status[data-orientation="auto"] .eq-order-status-track {
display: flex;
}
.eq-order-status[data-orientation="auto"] .eq-order-status-step {
flex: 1 1 0;
display: flex;
flex-direction: column;
gap: var(--eq-space-2);
padding: 0 var(--eq-space-3) 0 0;
}
.eq-order-status[data-orientation="auto"] .eq-order-status-step:last-child {
padding-inline-end: 0;
}
.eq-order-status[data-orientation="auto"] .eq-order-status-line {
top: calc(var(--_m) / 2 - 1px);
bottom: auto;
left: calc(var(--_m) + var(--eq-space-1));
right: var(--eq-space-1);
width: auto;
height: 2px;
}
.eq-order-status[data-orientation="auto"] .eq-order-status-line::after {
scale: 0 1;
transform-origin: left center;
}
.eq-order-status[data-orientation="auto"] .eq-order-status-line[data-filled]::after {
scale: 1;
}
.eq-order-status[data-orientation="auto"] .eq-order-status-text {
padding-top: 0;
}
}
/* ---- exception ---- */
.eq-order-status-exception {
display: flex;
align-items: flex-start;
gap: var(--eq-space-2);
padding: var(--eq-space-2) var(--eq-space-3);
border: 1px solid color-mix(in srgb, var(--_tone) 35%, transparent);
border-radius: var(--eq-radius-m);
background: color-mix(in srgb, var(--_tone) 8%, transparent);
}
.eq-order-status-exception > svg {
margin-top: 2px;
color: var(--_tone);
}
.eq-order-status-exception-text {
margin: 0;
min-width: 0;
}
.eq-order-status-exception-title {
color: var(--_tone);
font-weight: var(--eq-weight-strong);
}
/* ---- carrier and tracking ---- */
.eq-order-status-tracking {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--eq-space-2) var(--eq-space-4);
padding-top: var(--eq-space-3);
border-top: 1px solid var(--eq-line);
}
.eq-order-status-carrier {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--eq-space-1) var(--eq-space-2);
margin: 0;
min-width: 0;
color: var(--eq-ink-2);
}
.eq-order-status-carrier-name {
color: var(--eq-ink);
font-weight: var(--eq-weight-strong);
}
.eq-order-status-link {
display: inline-flex;
align-items: center;
gap: var(--eq-space-1);
min-height: 24px;
border-radius: var(--eq-radius-s);
color: var(--eq-ink);
font-weight: var(--eq-weight-strong);
text-decoration: underline;
text-decoration-color: var(--eq-line-strong);
text-underline-offset: 3px;
transition:
text-decoration-color var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease);
}
@media (hover: hover) {
.eq-order-status-link:hover {
text-decoration-color: currentColor;
}
}
.eq-order-status-link:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
.eq-order-status-link:active {
scale: var(--eq-press);
}
.eq-order-status-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
}