Order tracking timeline
Where’s my package, answered on one screen. A vertical
md-stepper plays the shipment timeline — completed
scans above, the current leg expanded, future stages greyed out and
unreachable. The header pairs an md-chip state token
with a pulsing md-status-dot for the live carrier
feed, an md-meter reads out kilometres covered on the
route, and an md-side-sheet keeps contact options
one tap away without leaving the page.
Payment of 249,90 lei confirmed. Two items reserved at the Bucharest warehouse.
Sealed in one parcel (1.8 kg) and handed to Curier Express at gate D3.
Departed the Sibiu sorting hub on line-haul RO-114. Next scan expected at the Cluj-Napoca depot around 13:30.
Courier Andrei V. is on the route — your stop is number 8 of 22.
Parcel handed over at the door. A signed proof of delivery is attached to this order.
Order #48213 · AWB 3805 5521 0021
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<div class="order-status">
<header>
<h2>Order #48213</h2>
<md-chip label="In transit" color="info" appearance="filled"></md-chip>
<span style="position: relative; display: inline-flex;">
<md-status-dot state="online" live label="Carrier feed live"></md-status-dot>
</span>
</header>
<md-meter label="Route progress — Bucharest to Cluj-Napoca"
max="449" value="302" value-text="302 km of 449 km"
show-label show-value></md-meter>
<md-stepper orientation="vertical" nav="false" auto-complete="false"
active="2" label="Shipment timeline">
<md-step label="Order placed" description="Mon 18 Aug, 09:14" completed editable>
Payment confirmed. Two items reserved at the Bucharest warehouse.
</md-step>
<md-step label="Packed" description="Mon 18 Aug, 16:40" completed editable>
Sealed in one parcel and handed to Curier Express.
</md-step>
<md-step label="In transit" description="Last scan: Sibiu hub, 07:52">
Departed the Sibiu sorting hub on line-haul RO-114.
</md-step>
<md-step label="Out for delivery" description="Estimated Wed 20 Aug"></md-step>
<md-step label="Delivered" description="Signature required"></md-step>
</md-stepper>
<md-button variant="outlined">Contact support</md-button>
</div>
<md-side-sheet variant="modal" headline="Delivery support" detached top-divider bottom-divider>
<md-list>
<md-list-item type="link" href="tel:+40723118402" leading-icon="call"
headline="Call the courier" supporting-text="Andrei V. — +40 723 118 402"></md-list-item>
<md-list-item type="link" href="mailto:support@curierexpress.ro" leading-icon="mail"
headline="Email support" supporting-text="Replies within one business day"></md-list-item>
<md-list-item type="button" leading-icon="report"
headline="Report a problem" supporting-text="Damaged, delayed, or missing parcel"></md-list-item>
</md-list>
<md-button slot="actions" variant="text">Done</md-button>
</md-side-sheet>
<script type="module">
const chip = document.querySelector('[data-state-chip]');
const dot = document.querySelector('[data-feed-dot]');
const feedText = document.querySelector('[data-feed-text]');
const meter = document.querySelector('[data-route-meter]');
const timeline = document.querySelector('[data-timeline]');
const steps = timeline.querySelectorAll('md-step');
const advance = document.querySelector('[data-advance]');
const contact = document.querySelector('[data-contact]');
const sheet = document.querySelector('[data-support-sheet]');
const done = document.querySelector('[data-sheet-done]');
// Each simulated scan: complete the current stage, activate the next,
// and keep the header chip + route meter in sync with the timeline.
const scans = [
{ chip: 'Out for delivery', color: 'tertiary', km: 437 },
{ chip: 'Delivered', color: 'success', km: 449 }
];
let scan = 0;
advance.addEventListener('mdClick', () => {
const next = scans[scan];
steps[timeline.active].completed = true;
timeline.active = timeline.active + 1;
chip.label = next.chip;
chip.color = next.color;
meter.value = next.km;
meter.valueText = next.km + ' km of 449 km';
scan = scan + 1;
if (scan === scans.length) {
steps[timeline.active].completed = true;
meter.color = 'success';
// Setting state and label together: the live dot announces the change.
dot.state = 'neutral';
dot.live = false;
dot.label = 'Carrier feed closed';
feedText.textContent = 'Feed closed';
advance.disabled = true;
advance.textContent = 'Shipment delivered';
}
});
// Slotted side-sheet controls are never auto-wired — open and close it here.
contact.addEventListener('mdClick', () => sheet.show());
done.addEventListener('mdClick', () => sheet.close());
document.querySelector('md-list-item[type="button"]').addEventListener('mdClick', () => sheet.close());
</script>import { useState } from 'react';
import {
MdButton, MdChip, MdList, MdListItem, MdMeter, MdSideSheet,
MdStatusDot, MdStep, MdStepper,
} from '@awc-ui/react';
const STAGES = [
{ label: 'Order placed', description: 'Mon 18 Aug, 09:14',
detail: 'Payment of 249,90 lei confirmed. Two items reserved at the Bucharest warehouse.' },
{ label: 'Packed', description: 'Mon 18 Aug, 16:40',
detail: 'Sealed in one parcel (1.8 kg) and handed to Curier Express at gate D3.' },
{ label: 'In transit', description: 'Last scan: Sibiu hub, 07:52',
detail: 'Departed the Sibiu sorting hub on line-haul RO-114.' },
{ label: 'Out for delivery', description: 'Estimated Wed 20 Aug',
detail: 'Courier Andrei V. is on the route — your stop is number 8 of 22.' },
{ label: 'Delivered', description: 'Signature required',
detail: 'Parcel handed over at the door. A signed proof of delivery is attached to this order.' },
];
// One scan index drives everything: the chip, the feed dot, the meter,
// and which timeline stages are completed.
const SCANS = [
{ chip: 'In transit', color: 'info', km: 302 },
{ chip: 'Out for delivery', color: 'tertiary', km: 437 },
{ chip: 'Delivered', color: 'success', km: 449 },
];
export default function OrderTracking() {
const [scan, setScan] = useState(0);
const [supportOpen, setSupportOpen] = useState(false);
const active = 2 + scan;
const delivered = scan === SCANS.length - 1;
const current = SCANS[scan];
return (
<div style={{ display: 'grid', gap: 24, maxWidth: 620 }}>
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
<h2 style={{ margin: 0 }}>Order #48213</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<MdChip label={current.chip} color={current.color} appearance="filled" />
<span style={{ position: 'relative', display: 'inline-flex' }}>
<MdStatusDot state={delivered ? 'neutral' : 'online'} live={!delivered}
label={delivered ? 'Carrier feed closed' : 'Carrier feed live'} />
</span>
<span>{delivered ? 'Feed closed' : 'Live feed'}</span>
</div>
</header>
<MdMeter label="Route progress — Bucharest to Cluj-Napoca" max={449}
value={current.km} valueText={current.km + ' km of 449 km'}
color={delivered ? 'success' : 'primary'} showLabel showValue />
<MdStepper orientation="vertical" nav={false} autoComplete={false}
active={active} label="Shipment timeline">
{STAGES.map((s, i) => (
<MdStep key={s.label} label={s.label} description={s.description}
completed={i < active || (delivered && i === active)}
editable={i < active}>
<p style={{ margin: 0 }}>{s.detail}</p>
</MdStep>
))}
</MdStepper>
<div style={{ display: 'flex', gap: 12 }}>
<MdButton variant="tonal" disabled={delivered} onMdClick={() => setScan(scan + 1)}>
{delivered ? 'Shipment delivered' : 'Simulate carrier scan'}
</MdButton>
<MdButton variant="outlined" onMdClick={() => setSupportOpen(true)}>Contact support</MdButton>
</div>
<MdSideSheet variant="modal" headline="Delivery support" detached topDivider bottomDivider
open={supportOpen} onMdClose={() => setSupportOpen(false)}>
<p>Order #48213 · AWB 3805 5521 0021</p>
<MdList>
<MdListItem type="link" href="tel:+40723118402" leadingIcon="call"
headline="Call the courier" supportingText="Andrei V. — +40 723 118 402" />
<MdListItem type="link" href="mailto:support@curierexpress.ro" leadingIcon="mail"
headline="Email support" supportingText="Replies within one business day" />
<MdListItem type="button" leadingIcon="report" headline="Report a problem"
supportingText="Damaged, delayed, or missing parcel"
onMdClick={() => setSupportOpen(false)} />
</MdList>
<MdButton slot="actions" variant="text" onMdClick={() => setSupportOpen(false)}>Done</MdButton>
</MdSideSheet>
</div>
);
}// order-tracking.component.ts — standalone; the md-* tags are used directly
// (elements registered once in main.ts: import '@awc-ui/core/define';)
import { Component, CUSTOM_ELEMENTS_SCHEMA, computed, signal } from '@angular/core';
const STAGES = [
{ label: 'Order placed', description: 'Mon 18 Aug, 09:14',
detail: 'Payment of 249,90 lei confirmed. Two items reserved at the Bucharest warehouse.' },
{ label: 'Packed', description: 'Mon 18 Aug, 16:40',
detail: 'Sealed in one parcel (1.8 kg) and handed to Curier Express at gate D3.' },
{ label: 'In transit', description: 'Last scan: Sibiu hub, 07:52',
detail: 'Departed the Sibiu sorting hub on line-haul RO-114.' },
{ label: 'Out for delivery', description: 'Estimated Wed 20 Aug',
detail: 'Courier Andrei V. is on the route — your stop is number 8 of 22.' },
{ label: 'Delivered', description: 'Signature required',
detail: 'Parcel handed over at the door. A signed proof of delivery is attached to this order.' },
];
// One scan index drives the chip, the feed dot, the meter, and the timeline.
const SCANS = [
{ chip: 'In transit', color: 'info', km: 302 },
{ chip: 'Out for delivery', color: 'tertiary', km: 437 },
{ chip: 'Delivered', color: 'success', km: 449 },
];
@Component({
selector: 'app-order-tracking',
standalone: true,
// Without this the compiler rejects the property bindings on md-* tags.
schemas: [CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './order-tracking.component.html',
})
export class OrderTrackingComponent {
stages = STAGES;
scan = signal(0);
supportOpen = signal(false);
active = computed(() => 2 + this.scan());
delivered = computed(() => this.scan() === SCANS.length - 1);
current = computed(() => SCANS[this.scan()]);
advance() { if (!this.delivered()) this.scan.update((s) => s + 1); }
}
<!-- order-tracking.component.html -->
<div class="order-status">
<header>
<h2>Order #48213</h2>
<md-chip [label]="current().chip" [color]="current().color" appearance="filled"></md-chip>
<span style="position: relative; display: inline-flex;">
<md-status-dot [state]="delivered() ? 'neutral' : 'online'" [live]="!delivered()"
[label]="delivered() ? 'Carrier feed closed' : 'Carrier feed live'"></md-status-dot>
</span>
<span>{{ delivered() ? 'Feed closed' : 'Live feed' }}</span>
</header>
<md-meter label="Route progress — Bucharest to Cluj-Napoca" max="449"
[value]="current().km" [valueText]="current().km + ' km of 449 km'"
[color]="delivered() ? 'success' : 'primary'" show-label show-value></md-meter>
<md-stepper orientation="vertical" nav="false" auto-complete="false"
[active]="active()" label="Shipment timeline">
@for (s of stages; track s.label; let i = $index) {
<md-step [label]="s.label" [description]="s.description"
[completed]="i < active() || (delivered() && i === active())"
[editable]="i < active()">
<p>{{ s.detail }}</p>
</md-step>
}
</md-stepper>
<div class="actions">
<md-button variant="tonal" [disabled]="delivered()" (mdClick)="advance()">
{{ delivered() ? 'Shipment delivered' : 'Simulate carrier scan' }}
</md-button>
<md-button variant="outlined" (mdClick)="supportOpen.set(true)">Contact support</md-button>
</div>
</div>
<md-side-sheet variant="modal" headline="Delivery support" detached top-divider bottom-divider
[open]="supportOpen()" (mdClose)="supportOpen.set(false)">
<p>Order #48213 · AWB 3805 5521 0021</p>
<md-list>
<md-list-item type="link" href="tel:+40723118402" leading-icon="call"
headline="Call the courier" supporting-text="Andrei V. — +40 723 118 402"></md-list-item>
<md-list-item type="link" href="mailto:support@curierexpress.ro" leading-icon="mail"
headline="Email support" supporting-text="Replies within one business day"></md-list-item>
<md-list-item type="button" leading-icon="report" headline="Report a problem"
supporting-text="Damaged, delayed, or missing parcel" (mdClick)="supportOpen.set(false)"></md-list-item>
</md-list>
<md-button slot="actions" variant="text" (mdClick)="supportOpen.set(false)">Done</md-button>
</md-side-sheet><script setup lang="ts">
import { computed, ref } from 'vue';
import {
MdButton, MdChip, MdList, MdListItem, MdMeter, MdSideSheet,
MdStatusDot, MdStep, MdStepper,
} from '@awc-ui/vue';
const STAGES = [
{ label: 'Order placed', description: 'Mon 18 Aug, 09:14',
detail: 'Payment of 249,90 lei confirmed. Two items reserved at the Bucharest warehouse.' },
{ label: 'Packed', description: 'Mon 18 Aug, 16:40',
detail: 'Sealed in one parcel (1.8 kg) and handed to Curier Express at gate D3.' },
{ label: 'In transit', description: 'Last scan: Sibiu hub, 07:52',
detail: 'Departed the Sibiu sorting hub on line-haul RO-114.' },
{ label: 'Out for delivery', description: 'Estimated Wed 20 Aug',
detail: 'Courier Andrei V. is on the route — your stop is number 8 of 22.' },
{ label: 'Delivered', description: 'Signature required',
detail: 'Parcel handed over at the door. A signed proof of delivery is attached to this order.' },
];
// One scan index drives the chip, the feed dot, the meter, and the timeline.
const SCANS = [
{ chip: 'In transit', color: 'info', km: 302 },
{ chip: 'Out for delivery', color: 'tertiary', km: 437 },
{ chip: 'Delivered', color: 'success', km: 449 },
];
const scan = ref(0);
const supportOpen = ref(false);
const active = computed(() => 2 + scan.value);
const delivered = computed(() => scan.value === SCANS.length - 1);
const current = computed(() => SCANS[scan.value]);
function advance() { if (!delivered.value) scan.value += 1; }
</script>
<template>
<div class="order-status">
<header>
<h2>Order #48213</h2>
<MdChip :label="current.chip" :color="current.color" appearance="filled" />
<span style="position: relative; display: inline-flex;">
<MdStatusDot :state="delivered ? 'neutral' : 'online'" :live="!delivered"
:label="delivered ? 'Carrier feed closed' : 'Carrier feed live'" />
</span>
<span>{{ delivered ? 'Feed closed' : 'Live feed' }}</span>
</header>
<MdMeter label="Route progress — Bucharest to Cluj-Napoca" :max="449"
:value="current.km" :valueText="current.km + ' km of 449 km'"
:color="delivered ? 'success' : 'primary'" :showLabel="true" :showValue="true" />
<MdStepper orientation="vertical" :nav="false" :autoComplete="false"
:active="active" label="Shipment timeline">
<MdStep v-for="(s, i) in STAGES" :key="s.label" :label="s.label" :description="s.description"
:completed="i < active || (delivered && i === active)" :editable="i < active">
<p>{{ s.detail }}</p>
</MdStep>
</MdStepper>
<div class="actions">
<MdButton variant="tonal" :disabled="delivered" @md-click="advance">
{{ delivered ? 'Shipment delivered' : 'Simulate carrier scan' }}
</MdButton>
<MdButton variant="outlined" @md-click="supportOpen = true">Contact support</MdButton>
</div>
<MdSideSheet variant="modal" headline="Delivery support" :detached="true"
:topDivider="true" :bottomDivider="true"
:open="supportOpen" @md-close="supportOpen = false">
<p>Order #48213 · AWB 3805 5521 0021</p>
<MdList>
<MdListItem type="link" href="tel:+40723118402" leadingIcon="call"
headline="Call the courier" supportingText="Andrei V. — +40 723 118 402" />
<MdListItem type="link" href="mailto:support@curierexpress.ro" leadingIcon="mail"
headline="Email support" supportingText="Replies within one business day" />
<MdListItem type="button" leadingIcon="report" headline="Report a problem"
supportingText="Damaged, delayed, or missing parcel" @md-click="supportOpen = false" />
</MdList>
<MdButton slot="actions" variant="text" @md-click="supportOpen = false">Done</MdButton>
</MdSideSheet>
</div>
</template><script>
// Register the md-* elements once on the client. In SvelteKit, guard it:
// if (browser) defineCustomElements(window);
import { defineCustomElements } from '@awc-ui/svelte';
defineCustomElements(window);
const STAGES = [
{ label: 'Order placed', description: 'Mon 18 Aug, 09:14',
detail: 'Payment of 249,90 lei confirmed. Two items reserved at the Bucharest warehouse.' },
{ label: 'Packed', description: 'Mon 18 Aug, 16:40',
detail: 'Sealed in one parcel (1.8 kg) and handed to Curier Express at gate D3.' },
{ label: 'In transit', description: 'Last scan: Sibiu hub, 07:52',
detail: 'Departed the Sibiu sorting hub on line-haul RO-114.' },
{ label: 'Out for delivery', description: 'Estimated Wed 20 Aug',
detail: 'Courier Andrei V. is on the route — your stop is number 8 of 22.' },
{ label: 'Delivered', description: 'Signature required',
detail: 'Parcel handed over at the door. A signed proof of delivery is attached to this order.' },
];
// One scan index drives the chip, the feed dot, the meter, and the timeline.
const SCANS = [
{ chip: 'In transit', color: 'info', km: 302 },
{ chip: 'Out for delivery', color: 'tertiary', km: 437 },
{ chip: 'Delivered', color: 'success', km: 449 },
];
let scan = 0;
let supportOpen = false;
$: active = 2 + scan;
$: delivered = scan === SCANS.length - 1;
$: current = SCANS[scan];
function advance() { if (!delivered) scan += 1; }
</script>
<div class="order-status">
<header>
<h2>Order #48213</h2>
<md-chip label={current.chip} color={current.color} appearance="filled"></md-chip>
<span style="position: relative; display: inline-flex;">
<md-status-dot state={delivered ? 'neutral' : 'online'} live={!delivered}
label={delivered ? 'Carrier feed closed' : 'Carrier feed live'}></md-status-dot>
</span>
<span>{delivered ? 'Feed closed' : 'Live feed'}</span>
</header>
<md-meter label="Route progress — Bucharest to Cluj-Napoca" max="449"
value={current.km} value-text={current.km + ' km of 449 km'}
color={delivered ? 'success' : 'primary'} show-label show-value></md-meter>
<md-stepper orientation="vertical" nav="false" auto-complete="false"
active={active} label="Shipment timeline">
{#each STAGES as s, i (s.label)}
<md-step label={s.label} description={s.description}
completed={i < active || (delivered && i === active)} editable={i < active}>
<p>{s.detail}</p>
</md-step>
{/each}
</md-stepper>
<div class="actions">
<md-button variant="tonal" disabled={delivered} on:mdClick={advance}>
{delivered ? 'Shipment delivered' : 'Simulate carrier scan'}
</md-button>
<md-button variant="outlined" on:mdClick={() => (supportOpen = true)}>Contact support</md-button>
</div>
</div>
<md-side-sheet variant="modal" headline="Delivery support" detached top-divider bottom-divider
open={supportOpen} on:mdClose={() => (supportOpen = false)}>
<p>Order #48213 · AWB 3805 5521 0021</p>
<md-list>
<md-list-item type="link" href="tel:+40723118402" leading-icon="call"
headline="Call the courier" supporting-text="Andrei V. — +40 723 118 402"></md-list-item>
<md-list-item type="link" href="mailto:support@curierexpress.ro" leading-icon="mail"
headline="Email support" supporting-text="Replies within one business day"></md-list-item>
<md-list-item type="button" leading-icon="report" headline="Report a problem"
supporting-text="Damaged, delayed, or missing parcel" on:mdClick={() => (supportOpen = false)}></md-list-item>
</md-list>
<md-button slot="actions" variant="text" on:mdClick={() => (supportOpen = false)}>Done</md-button>
</md-side-sheet>How it’s built
Section titled “How it’s built”| Component | Role in this screen |
|---|---|
md-stepper | The timeline. orientation="vertical" gives each stage an inline expanding panel; nav="false" removes Back / Continue because a shipment is not user-driven; auto-complete="false" hands completed to the carrier feed instead of to clicks. |
md-step | One scan event each — label for the stage, description for the timestamp, panel content for the detail. Past stages are completed editable so revisiting them never rewrites downstream state. |
md-chip | The headline state token. color takes theme role names, so the same chip walks info → tertiary → success as the parcel moves. |
md-status-dot | The carrier-feed pip. label + live make it a role="status" live region, so “Carrier feed closed” is announced when tracking ends. |
md-meter | Route progress as a state, not an activity. max="449" with value-text="302 km of 449 km" shows kilometres while aria-valuetext says exactly the same string. |
md-side-sheet | Contact options in a variant="modal" overlay — scrim, focus trap, and Escape handling included, with an md-list of call / email / report rows. |
The details that make it production-grade
Section titled “The details that make it production-grade”- The timeline can’t lie about the future. In the stepper’s default
mode="linear", stages after the active one are unreachable — their headers renderaria-disabledand leave the tab order — so a customer can review past scans but never “open” a delivery that hasn’t happened. - Completion belongs to the carrier, not the click. With
auto-complete="false"the checkmarks only change when the feed says so; revisiting “Order placed” never un-completes “Packed” the way the default auto-complete would on a backward move. - The status dot is a real live region. Because both
labelandliveare set it exposesrole="status", so the switch to “Carrier feed closed” is announced politely — and its pulse is already gated behindprefers-reduced-motion, with no extra wiring. - One string drives what is seen and what is heard. The meter’s
value-textoverrides both the visible header value andaria-valuetext, so “437 km of 449 km” can never drift from what a screen reader announces. - The support sheet has a guaranteed exit.
variant="modal"traps focus and locks body scroll, butcloseable(on by default), the scrim, and Escape all dismiss it — and focus returns to the “Contact support” button automatically on close.
Variations
Section titled “Variations”- Compact tracking widget: swap the meter to
variant="circular" size="72" show-valueand place it beside the chip for a dashboard-tile version of the same reading. - Exception states: a failed delivery attempt is
error+error-text="Delivery attempted — nobody home"on the affected step; the stepper announces it assertively and the glyph switches to the error mark. - Denser timeline for order history: set
density="-2"on the stepper to compact indicators and connectors when the page lists several shipments. - Mobile: replace the side sheet with an
md-bottom-sheetbelow ~600px — side sheets are a large-screen surface by design.
Related
Section titled “Related”- Recipe: Checkout wizard — the same stepper driving an interactive flow instead of a read-only timeline.
- Recipe: Server fleet status — status dots and meters reporting health at fleet scale.
- Component manuals:
md-stepper,md-side-sheet,md-meter,md-status-dot— full APIs, a11y contracts, and anti-pattern lists.