UI Components Medium
Restaurant Cart / Order Summary
Customer order summary widget — line items with modifiers, quantity steppers, subtotal/tax/tip/total, gratuity presets and a send-to-kitchen CTA.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(44, 26, 14, 0.08), 0 2px 6px rgba(44, 26, 14, 0.06);
--shadow-2: 0 8px 24px rgba(44, 26, 14, 0.12);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
min-height: 100vh;
padding: 32px 16px 48px;
display: flex;
justify-content: center;
-webkit-font-smoothing: antialiased;
}
.ticket {
width: 100%;
max-width: 460px;
background: var(--bone);
border-radius: var(--r-lg);
border: 1px solid rgba(44, 26, 14, 0.08);
box-shadow: var(--shadow-2);
padding: 22px 22px 18px;
display: flex;
flex-direction: column;
gap: 18px;
}
/* ── Head ── */
.ticket-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
padding-bottom: 14px;
border-bottom: 1px dashed rgba(44, 26, 14, 0.18);
}
.ticket-kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--terracotta);
font-weight: 600;
margin-bottom: 4px;
}
.ticket-title {
font-family: var(--font-display);
font-size: 1.55rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.ticket-count {
font-size: 0.78rem;
color: var(--warm-gray);
font-weight: 500;
padding-top: 4px;
white-space: nowrap;
}
/* ── Lines ── */
.lines {
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.line {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 10px 0;
border-bottom: 1px solid rgba(44, 26, 14, 0.06);
animation: lineIn 0.18s ease;
}
.line:last-child {
border-bottom: none;
}
@keyframes lineIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: none;
}
}
.line-body {
flex: 1;
min-width: 0;
}
.line-name {
font-weight: 600;
font-size: 0.95rem;
color: var(--ink);
letter-spacing: -0.005em;
margin-bottom: 2px;
}
.line-mods {
font-size: 0.78rem;
color: var(--warm-gray);
line-height: 1.45;
}
.line-mods span + span::before {
content: " · ";
color: rgba(44, 26, 14, 0.3);
}
.line-controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.qty {
display: inline-flex;
align-items: center;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 2px;
gap: 2px;
}
.qty-btn {
width: 24px;
height: 24px;
border-radius: 999px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
color: var(--ink);
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
display: grid;
place-items: center;
line-height: 1;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.qty-btn:hover {
background: var(--cream-2);
color: var(--terracotta-d);
}
.qty-num {
min-width: 22px;
text-align: center;
font-size: 0.82rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.line-remove {
background: none;
border: none;
color: var(--warm-gray);
font-size: 0.78rem;
font-family: inherit;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: color 0.15s, background 0.15s;
}
.line-remove:hover {
color: var(--danger);
background: rgba(179, 67, 42, 0.08);
}
.line-price {
font-family: var(--font-mono);
font-size: 0.92rem;
font-weight: 700;
color: var(--ink);
white-space: nowrap;
padding-top: 2px;
}
.empty {
text-align: center;
font-style: italic;
color: var(--warm-gray);
padding: 24px 12px;
}
/* ── Tip ── */
.tip {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.tip-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-2);
font-weight: 700;
}
.tip-presets {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tip-btn {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.12);
color: var(--ink-2);
border-radius: 999px;
padding: 6px 12px;
font-family: inherit;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.tip-btn:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.tip-btn.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.tip-custom {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--ink-2);
}
.tip-custom input {
width: 80px;
padding: 6px 10px;
border: 1px solid rgba(44, 26, 14, 0.12);
background: var(--bone);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 0.85rem;
font-weight: 600;
color: var(--ink);
outline: none;
text-align: center;
}
.tip-custom input:focus {
border-color: var(--terracotta);
}
/* ── Summary ── */
.summary {
display: flex;
flex-direction: column;
gap: 4px;
padding-top: 4px;
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 0.88rem;
color: var(--ink-2);
}
.summary-row dd {
font-family: var(--font-mono);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.summary-row--total {
margin-top: 6px;
padding-top: 12px;
border-top: 1px solid rgba(44, 26, 14, 0.18);
font-size: 1.1rem;
font-weight: 700;
color: var(--ink);
}
.summary-row--total dd {
font-size: 1.2rem;
color: var(--ink);
}
.tip-tag {
display: inline-block;
background: var(--cream-2);
color: var(--ink-2);
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 2px 7px;
border-radius: 999px;
margin-left: 4px;
font-family: var(--font-mono);
}
/* ── Footer ── */
.ticket-foot {
display: flex;
gap: 10px;
}
.ghost {
background: transparent;
border: 1px solid rgba(44, 26, 14, 0.18);
color: var(--ink-2);
border-radius: 999px;
padding: 11px 18px;
font-family: inherit;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.ghost:hover {
background: var(--cream-2);
color: var(--ink);
}
.cta {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--forest);
color: var(--bone);
border: none;
border-radius: 999px;
padding: 12px 18px;
font-family: inherit;
font-size: 0.92rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, transform 0.15s;
}
.cta:hover {
background: var(--forest-d);
}
.cta:disabled {
background: var(--warm-gray);
cursor: not-allowed;
opacity: 0.7;
}
.cta:active:not(:disabled) {
transform: scale(0.98);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
background: var(--forest-d);
color: var(--bone);
padding: 10px 18px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: var(--shadow-2);
animation: toastIn 0.25s ease;
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%);
}
}
/* Visibility guard: honor the [hidden] attribute over base display */
.tip-custom[hidden] {
display: none;
}const TAX_RATE = 0.0825;
const STARTING_ORDER = [
{
id: "burrata",
name: "Burrata de la huerta",
price: 16,
qty: 1,
mods: ["Add focaccia"],
},
{
id: "ribeye",
name: "Ribeye 14oz",
price: 48,
qty: 1,
mods: ["Medium rare", "Truffle fries (+$4)", "Bone marrow (+$6)"],
},
{
id: "risotto",
name: "Risotto de hongos",
price: 26,
qty: 1,
mods: ["No parmesan"],
},
{
id: "tarta",
name: "Tarta de queso quemada",
price: 11,
qty: 2,
mods: [],
},
];
let order = STARTING_ORDER.map((i) => ({ ...i }));
let tipMode = "18"; // "0" | "15" | "18" | "20" | "custom"
let customTip = 22;
const linesEl = document.getElementById("lines");
const emptyEl = document.getElementById("empty");
const countEl = document.getElementById("count");
const subtotalEl = document.getElementById("subtotal");
const taxEl = document.getElementById("tax");
const tipAmountEl = document.getElementById("tipAmount");
const tipTagEl = document.getElementById("tipTag");
const totalEl = document.getElementById("total");
const tipBtns = document.querySelectorAll(".tip-btn");
const tipCustomWrap = document.getElementById("tipCustomWrap");
const tipCustomInput = document.getElementById("tipCustom");
const sendBtn = document.getElementById("send");
const resetBtn = document.getElementById("reset");
const toast = document.getElementById("toast");
function money(value) {
return `$${value.toFixed(2)}`;
}
function tipPercent() {
if (tipMode === "custom") return Math.max(0, Math.min(100, customTip)) / 100;
return Number(tipMode) / 100;
}
function renderLines() {
if (order.length === 0) {
linesEl.innerHTML = "";
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
linesEl.innerHTML = order
.map(
(line) => `
<li class="line" data-id="${line.id}">
<div class="line-body">
<p class="line-name">${line.name}</p>
${
line.mods.length
? `<p class="line-mods">${line.mods.map((m) => `<span>${m}</span>`).join("")}</p>`
: ""
}
<div class="line-controls">
<div class="qty" aria-label="Quantity">
<button class="qty-btn" data-action="dec" aria-label="Decrease">−</button>
<span class="qty-num">${line.qty}</span>
<button class="qty-btn" data-action="inc" aria-label="Increase">+</button>
</div>
<button class="line-remove" data-action="remove">Remove</button>
</div>
</div>
<span class="line-price">${money(line.price * line.qty)}</span>
</li>`
)
.join("");
}
function renderTotals() {
const subtotal = order.reduce((s, l) => s + l.price * l.qty, 0);
const tax = subtotal * TAX_RATE;
const tipPct = tipPercent();
const tipValue = subtotal * tipPct;
const total = subtotal + tax + tipValue;
const itemCount = order.reduce((n, l) => n + l.qty, 0);
countEl.textContent = `${itemCount} ${itemCount === 1 ? "item" : "items"}`;
subtotalEl.textContent = money(subtotal);
taxEl.textContent = money(tax);
tipAmountEl.textContent = money(tipValue);
totalEl.textContent = money(total);
tipTagEl.textContent = `${Math.round(tipPct * 100)}%`;
sendBtn.disabled = order.length === 0;
}
function render() {
renderLines();
renderTotals();
}
linesEl.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const li = btn.closest("[data-id]");
if (!li) return;
const line = order.find((l) => l.id === li.dataset.id);
if (!line) return;
const action = btn.dataset.action;
if (action === "inc") line.qty = Math.min(99, line.qty + 1);
if (action === "dec") {
line.qty -= 1;
if (line.qty <= 0) order = order.filter((l) => l.id !== line.id);
}
if (action === "remove") order = order.filter((l) => l.id !== line.id);
render();
});
tipBtns.forEach((btn) => {
btn.addEventListener("click", () => {
tipBtns.forEach((b) => b.classList.remove("is-active"));
btn.classList.add("is-active");
tipMode = btn.dataset.tip;
tipCustomWrap.hidden = tipMode !== "custom";
renderTotals();
});
});
tipCustomInput.addEventListener("input", (e) => {
customTip = Number(e.target.value) || 0;
if (tipMode === "custom") renderTotals();
});
sendBtn.addEventListener("click", () => {
toast.hidden = false;
toast.textContent = `Order sent to kitchen · ${totalEl.textContent}`;
clearTimeout(sendBtn._t);
sendBtn._t = setTimeout(() => (toast.hidden = true), 2400);
});
resetBtn.addEventListener("click", () => {
order = STARTING_ORDER.map((i) => ({ ...i }));
tipMode = "18";
customTip = 22;
tipBtns.forEach((b) => b.classList.toggle("is-active", b.dataset.tip === "18"));
tipCustomWrap.hidden = true;
tipCustomInput.value = 22;
render();
});
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Order Summary</title>
</head>
<body>
<section class="ticket" aria-label="Order summary">
<header class="ticket-head">
<div>
<p class="ticket-kicker">Table 7 · 2 guests</p>
<h1 class="ticket-title">Your Order</h1>
</div>
<span class="ticket-count" id="count">0 items</span>
</header>
<ul class="lines" id="lines"></ul>
<p class="empty" id="empty" hidden>
Your order is empty. Add a dish from the carta to start.
</p>
<section class="tip" id="tip">
<p class="tip-label">Gratuity</p>
<div class="tip-presets" role="radiogroup" aria-label="Tip preset">
<button type="button" class="tip-btn" data-tip="0">No tip</button>
<button type="button" class="tip-btn" data-tip="15">15%</button>
<button type="button" class="tip-btn is-active" data-tip="18">18%</button>
<button type="button" class="tip-btn" data-tip="20">20%</button>
<button type="button" class="tip-btn" data-tip="custom">Custom</button>
</div>
<label class="tip-custom" id="tipCustomWrap" hidden>
<span>Custom %</span>
<input
id="tipCustom"
type="number"
min="0"
max="100"
step="1"
value="22"
inputmode="numeric"
/>
</label>
</section>
<dl class="summary">
<div class="summary-row">
<dt>Subtotal</dt>
<dd id="subtotal">$0.00</dd>
</div>
<div class="summary-row">
<dt>Tax (8.25%)</dt>
<dd id="tax">$0.00</dd>
</div>
<div class="summary-row" id="tipRow">
<dt>
Tip <span class="tip-tag" id="tipTag">18%</span>
</dt>
<dd id="tipAmount">$0.00</dd>
</div>
<div class="summary-row summary-row--total">
<dt>Total</dt>
<dd id="total">$0.00</dd>
</div>
</dl>
<footer class="ticket-foot">
<button type="button" class="ghost" id="reset">Reset</button>
<button type="button" class="cta" id="send">
<span>Send to kitchen</span>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path
d="M5 12h14M12 5l7 7-7 7"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
/>
</svg>
</button>
</footer>
</section>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Restaurant Cart / Order Summary
The cart surface diners see before sending an order to the kitchen, or that staff see on the POS ticket panel. Each line item displays modifiers under the dish name, a quantity stepper updates totals in place, tip presets (15 / 18 / 20 / custom) and an editable tip field drive the gratuity row, and a send-to-kitchen CTA fires when at least one item remains.
Adapted from the shopping-cart resource with restaurant-specific affordances (modifier sublines, tip presets, table number).