UI Components Easy
Takeout Pickup Status
Customer-facing takeout order status widget: order number lookup, animated step tracker (received → preparing → ready → picked up), and an estimated time countdown.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* =============================================
Takeout Pickup Status — Phase 27 Restaurant Theme
============================================= */
:root {
--cream: #FAF7F1;
--ink: #2C1A0E;
--forest: #345F40;
--forest-d: #213D29;
--terracotta: #C4622D;
--gold: #D4A853;
--warm-gray: #8A7D72;
--bone: #F0EBE0;
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 20px;
--shadow-card: 0 4px 24px rgba(44, 26, 14, 0.10);
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--cream);
color: var(--ink);
font-family: 'Inter', sans-serif;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 32px 16px 48px;
}
.page-wrapper {
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* ---- Header ---- */
.app-header {
text-align: center;
padding-bottom: 4px;
}
.restaurant-name {
font-family: 'Playfair Display', serif;
font-size: 2rem;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.5px;
line-height: 1.1;
}
.header-subtitle {
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--warm-gray);
margin-top: 4px;
}
/* ---- Lookup ---- */
.lookup-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.lookup-form {
display: flex;
gap: 8px;
}
.order-input {
flex: 1;
padding: 14px 16px;
border: 2px solid var(--bone);
border-radius: var(--radius-sm);
background: #fff;
color: var(--ink);
font-family: 'Inter', sans-serif;
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.04em;
outline: none;
transition: border-color 0.2s;
}
.order-input::placeholder {
font-weight: 400;
color: var(--warm-gray);
}
.order-input:focus {
border-color: var(--forest);
}
.check-btn {
padding: 14px 22px;
background: var(--forest);
color: var(--bone);
border: none;
border-radius: var(--radius-sm);
font-family: 'Inter', sans-serif;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
white-space: nowrap;
min-height: 48px;
}
.check-btn:hover {
background: var(--forest-d);
}
.check-btn:active {
transform: scale(0.97);
}
.lookup-hint {
font-size: 0.78rem;
color: var(--warm-gray);
padding-left: 4px;
}
.lookup-hint.error {
color: var(--terracotta);
font-weight: 600;
}
/* ---- Status card ---- */
.status-card {
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
padding: 28px 24px 24px;
display: flex;
flex-direction: column;
gap: 20px;
border: 1px solid var(--bone);
}
/* ---- Order meta ---- */
.order-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.order-number {
font-family: 'Playfair Display', serif;
font-size: 1.9rem;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.5px;
}
.order-items {
font-size: 0.85rem;
color: var(--warm-gray);
font-weight: 500;
}
/* ---- Ready banner ---- */
.ready-banner {
background: var(--forest);
color: var(--bone);
text-align: center;
font-family: 'Playfair Display', serif;
font-size: 1.2rem;
font-weight: 700;
padding: 20px;
border-radius: 10px;
display: none;
animation: bannerPulse 1.8s ease-in-out infinite;
}
.ready-banner.visible {
display: block;
}
@keyframes bannerPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(52, 95, 64, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(52, 95, 64, 0); }
}
/* ---- Step tracker ---- */
.step-tracker {
display: flex;
flex-direction: column;
gap: 0;
position: relative;
}
.step {
display: grid;
grid-template-columns: 40px 1fr;
grid-template-rows: auto auto;
column-gap: 16px;
align-items: center;
position: relative;
}
.step-icon-wrap {
grid-column: 1;
grid-row: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--bone);
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
transition: background 0.35s, border-color 0.35s;
position: relative;
z-index: 2;
}
.step-circle.completed {
background: var(--forest);
border-color: var(--forest);
}
.step-circle.active {
border-color: var(--gold);
animation: stepPulse 1.4s ease-in-out infinite;
}
@keyframes stepPulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(212, 168, 83, 0.35); }
50% { transform: scale(1.10); box-shadow: 0 0 0 6px rgba(212, 168, 83, 0); }
}
.step-info {
grid-column: 2;
grid-row: 1;
display: flex;
flex-direction: column;
gap: 2px;
padding: 10px 0;
}
.step-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--ink);
}
.step-label.muted {
color: var(--warm-gray);
font-weight: 500;
}
.step-time {
font-size: 0.75rem;
color: var(--warm-gray);
min-height: 1em;
}
/* Connecting line between steps */
.step-connector {
grid-column: 1;
width: 2px;
background: var(--bone);
margin: 0 auto;
transition: background 0.35s;
}
.step-connector.filled {
background: var(--forest);
}
.step-connector-top {
height: 10px;
}
.step-connector-bottom {
height: 10px;
}
/* ---- ETA bar ---- */
.eta-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.eta-label {
font-size: 0.85rem;
font-weight: 500;
color: var(--warm-gray);
display: flex;
align-items: center;
gap: 4px;
}
.eta-label strong {
font-size: 1rem;
font-weight: 700;
color: var(--ink);
}
.eta-track {
height: 8px;
background: var(--bone);
border-radius: 99px;
overflow: hidden;
}
.eta-fill {
height: 100%;
background: var(--gold);
border-radius: 99px;
width: 0%;
transition: width 2s cubic-bezier(0.4, 0, 0.2, 1);
}
.eta-section.done .eta-label {
color: var(--forest);
}
.eta-section.done .eta-label strong {
color: var(--forest);
}
/* ---- Simulate button ---- */
.sim-row {
display: flex;
justify-content: center;
}
.sim-btn {
padding: 11px 24px;
background: transparent;
color: var(--forest);
border: 2px solid var(--forest);
border-radius: var(--radius-sm);
font-family: 'Inter', sans-serif;
font-size: 0.88rem;
font-weight: 700;
cursor: pointer;
transition: background 0.2s, color 0.2s, transform 0.1s;
letter-spacing: 0.02em;
}
.sim-btn:hover {
background: var(--forest);
color: var(--bone);
}
.sim-btn:active {
transform: scale(0.97);
}
.sim-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}/* =============================================
Takeout Pickup Status — script.js
============================================= */
const ORDERS = {
'T-042': { items: ['Burrata huerta', 'Ribeye 14oz'], step: 0, eta: 18 },
'T-038': { items: ['Tarta de queso'], step: 2, eta: 3 },
'T-051': { items: ['Pulpo brasa', 'Pappardelle ragú', 'Negroni sbagliato'], step: 1, eta: 12 },
};
const STEPS = [
'Order received',
'Preparing your food',
'Ready for pickup',
'Order picked up',
];
const STEP_EMOJIS = ['📋', '👨🍳', '🛎️', '✅'];
const STEP_TIMES = ['20:31', '20:33', '', ''];
// Runtime step times — filled in as steps are simulated
const stepTimes = { ...{} };
let currentOrderKey = null;
let autoDemoTimer = null;
let autoDemoStep = 0;
// ---- DOM refs ----
const lookupForm = document.getElementById('lookupForm');
const orderInput = document.getElementById('orderInput');
const lookupHint = document.getElementById('lookupHint');
const orderNumberEl = document.getElementById('orderNumber');
const orderItemsEl = document.getElementById('orderItems');
const readyBanner = document.getElementById('readyBanner');
const etaMinutesEl = document.getElementById('etaMinutes');
const etaFill = document.getElementById('etaFill');
const etaSection = document.getElementById('etaSection');
const simBtn = document.getElementById('simBtn');
// ---- Helpers ----
function nowTime() {
const d = new Date();
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
return `${h}:${m}`;
}
function etaPercent(step, etaMin) {
if (step >= 3) return 100;
if (step === 2) return 90;
if (step === 1) return 55;
return 20;
}
// ---- Render ----
function render(orderKey) {
const order = ORDERS[orderKey];
if (!order) return;
const step = order.step;
const eta = order.eta;
// Meta
orderNumberEl.textContent = orderKey;
const count = order.items.length;
orderItemsEl.textContent = `${count} item${count !== 1 ? 's' : ''} · ${order.items.join(', ')}`;
// Steps
for (let i = 0; i < 4; i++) {
const circle = document.getElementById(`circle-${i}`);
const timeEl = document.getElementById(`time-${i}`);
const labelEl = circle?.closest('.step')?.querySelector('.step-label');
// Determine state
circle.classList.remove('completed', 'active');
if (i < step) {
// Completed
circle.classList.add('completed');
circle.innerHTML = `<span style="color:#fff;font-size:1rem;">✓</span>`;
if (labelEl) labelEl.classList.remove('muted');
} else if (i === step) {
// Active (current)
circle.classList.add('active');
circle.innerHTML = `<span class="step-emoji">${STEP_EMOJIS[i]}</span>`;
if (labelEl) labelEl.classList.remove('muted');
} else {
// Future
circle.innerHTML = `<span class="step-emoji" style="opacity:0.35;">${STEP_EMOJIS[i]}</span>`;
if (labelEl) labelEl.classList.add('muted');
}
// Times
const key = `${orderKey}_${i}`;
if (i < step) {
// Use persisted or defaults
timeEl.textContent = stepTimes[key] || STEP_TIMES[i] || '';
} else if (i === step) {
if (!stepTimes[key]) {
stepTimes[key] = nowTime();
}
timeEl.textContent = stepTimes[key];
} else {
timeEl.textContent = '';
}
// Connectors
const connectorBottom = document.getElementById(`connector-${i}`);
if (connectorBottom) {
if (i < step) {
connectorBottom.classList.add('filled');
} else {
connectorBottom.classList.remove('filled');
}
}
const connectorTop = document.getElementById(`connector-top-${i}`);
if (connectorTop) {
if (i <= step && step > 0) {
connectorTop.classList.add('filled');
} else {
connectorTop.classList.remove('filled');
}
}
}
// Ready banner
if (step === 2) {
readyBanner.classList.add('visible');
} else {
readyBanner.classList.remove('visible');
}
// ETA
if (step >= 3) {
etaSection.classList.add('done');
etaMinutesEl.textContent = '0';
etaSection.querySelector('.eta-label').innerHTML = '<span>Order</span> <strong>collected</strong> <span>— enjoy!</span>';
setTimeout(() => { etaFill.style.width = '100%'; }, 80);
} else {
etaSection.classList.remove('done');
etaSection.querySelector('.eta-label').innerHTML = `<span>Ready in approx</span> <strong id="etaMinutes">${eta}</strong> <span>min</span>`;
const pct = etaPercent(step, eta);
setTimeout(() => { etaFill.style.width = `${pct}%`; }, 80);
}
// Simulate button
simBtn.disabled = step >= 3;
simBtn.textContent = step >= 3 ? 'Order complete ✓' : 'Simulate next step →';
}
// ---- Lookup ----
lookupForm.addEventListener('submit', (e) => {
e.preventDefault();
const raw = orderInput.value.trim().toUpperCase();
if (!raw) return;
stopAutoDemo();
if (ORDERS[raw]) {
currentOrderKey = raw;
lookupHint.textContent = `Showing status for ${raw}`;
lookupHint.classList.remove('error');
render(currentOrderKey);
} else {
lookupHint.textContent = `Order "${raw}" not found. Try T-042, T-038, or T-051.`;
lookupHint.classList.add('error');
}
});
// ---- Simulate button ----
simBtn.addEventListener('click', () => {
if (!currentOrderKey) return;
const order = ORDERS[currentOrderKey];
if (order.step < 3) {
order.step += 1;
// Reduce eta
if (order.step === 1) order.eta = Math.max(1, order.eta - 6);
if (order.step === 2) order.eta = 0;
if (order.step === 3) order.eta = 0;
render(currentOrderKey);
}
});
// ---- Auto-demo ----
function startAutoDemo() {
autoDemoStep = 0;
ORDERS['T-042'].step = 0;
currentOrderKey = 'T-042';
render('T-042');
autoDemoTimer = setInterval(() => {
autoDemoStep = (autoDemoStep + 1) % 4;
ORDERS['T-042'].step = autoDemoStep;
// Reset eta for demo
ORDERS['T-042'].eta = [18, 12, 0, 0][autoDemoStep];
render('T-042');
if (autoDemoStep === 3) {
// Pause at final state, then restart
clearInterval(autoDemoTimer);
setTimeout(() => {
if (!orderInput.value.trim()) {
startAutoDemo();
}
}, 3500);
}
}, 3000);
}
function stopAutoDemo() {
if (autoDemoTimer) {
clearInterval(autoDemoTimer);
autoDemoTimer = null;
}
}
// ---- Init ----
startAutoDemo();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Takeout Pickup Status</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<header class="app-header">
<div class="restaurant-name">La Brasa</div>
<div class="header-subtitle">Order Status</div>
</header>
<!-- Lookup -->
<section class="lookup-section">
<form class="lookup-form" id="lookupForm">
<input
type="text"
id="orderInput"
class="order-input"
placeholder="Enter order # (e.g. T-042)"
maxlength="10"
autocomplete="off"
/>
<button type="submit" class="check-btn">Check</button>
</form>
<p class="lookup-hint" id="lookupHint">Try T-042, T-038, or T-051</p>
</section>
<!-- Status card -->
<section class="status-card" id="statusCard">
<!-- Order meta -->
<div class="order-meta">
<span class="order-number" id="orderNumber">T-042</span>
<span class="order-items" id="orderItems">2 items · Burrata huerta, Ribeye 14oz</span>
</div>
<!-- Ready banner -->
<div class="ready-banner" id="readyBanner">
YOUR ORDER IS READY 🔔
</div>
<!-- Step tracker -->
<div class="step-tracker" id="stepTracker">
<!-- Step 0 -->
<div class="step" id="step-0">
<div class="step-connector step-connector-top"></div>
<div class="step-icon-wrap">
<div class="step-circle" id="circle-0">
<span class="step-emoji">📋</span>
</div>
</div>
<div class="step-info">
<span class="step-label">Order received</span>
<span class="step-time" id="time-0"></span>
</div>
<div class="step-connector step-connector-bottom" id="connector-0"></div>
</div>
<!-- Step 1 -->
<div class="step" id="step-1">
<div class="step-connector step-connector-top" id="connector-top-1"></div>
<div class="step-icon-wrap">
<div class="step-circle" id="circle-1">
<span class="step-emoji">👨🍳</span>
</div>
</div>
<div class="step-info">
<span class="step-label">Preparing your food</span>
<span class="step-time" id="time-1"></span>
</div>
<div class="step-connector step-connector-bottom" id="connector-1"></div>
</div>
<!-- Step 2 -->
<div class="step" id="step-2">
<div class="step-connector step-connector-top" id="connector-top-2"></div>
<div class="step-icon-wrap">
<div class="step-circle" id="circle-2">
<span class="step-emoji">🛎️</span>
</div>
</div>
<div class="step-info">
<span class="step-label">Ready for pickup</span>
<span class="step-time" id="time-2"></span>
</div>
<div class="step-connector step-connector-bottom" id="connector-2"></div>
</div>
<!-- Step 3 -->
<div class="step" id="step-3">
<div class="step-connector step-connector-top" id="connector-top-3"></div>
<div class="step-icon-wrap">
<div class="step-circle" id="circle-3">
<span class="step-emoji">✅</span>
</div>
</div>
<div class="step-info">
<span class="step-label">Order picked up</span>
<span class="step-time" id="time-3"></span>
</div>
<div class="step-connector step-connector-bottom"></div>
</div>
</div>
<!-- ETA bar -->
<div class="eta-section" id="etaSection">
<div class="eta-label">
<span>Ready in approx</span>
<strong id="etaMinutes">18</strong>
<span>min</span>
</div>
<div class="eta-track">
<div class="eta-fill" id="etaFill"></div>
</div>
</div>
<!-- Simulate button -->
<div class="sim-row">
<button class="sim-btn" id="simBtn">Simulate next step →</button>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>Takeout Pickup Status
Order number input (e.g. “T-042”) → lookup triggers the status tracker. Four animated steps: Received ✓ → Preparing (animated pulse) → Ready for pickup 🔔 → Picked up. Each step has an icon, label, and timestamp. “Ready” state plays a subtle pulse animation and shows a large “YOUR ORDER IS READY” banner. Auto-demo mode cycles through states every few seconds if no order number entered.