UI Components Easy
Menu Item Detail
Restaurant dish detail card with image area, allergen chips, modifier picker (size, doneness, extras), quantity stepper and add-to-order CTA with live total.
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;
}
/* ── Card shell ── */
.card {
width: 100%;
max-width: 520px;
background: var(--bone);
border-radius: var(--r-lg);
border: 1px solid rgba(44, 26, 14, 0.08);
box-shadow: var(--shadow-2);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Hero ── */
.card-hero {
position: relative;
height: 200px;
background: radial-gradient(circle at 30% 70%, rgba(193, 113, 74, 0.35), transparent 60%),
radial-gradient(circle at 70% 30%, rgba(201, 168, 76, 0.25), transparent 55%),
linear-gradient(135deg, var(--cream-2) 0%, var(--cream) 100%);
display: grid;
place-items: center;
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
}
.hero-glyph {
font-size: 5rem;
filter: drop-shadow(0 6px 12px rgba(44, 26, 14, 0.18));
}
.back {
position: absolute;
top: 14px;
left: 14px;
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(250, 247, 241, 0.92);
color: var(--ink);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
font-size: 0.78rem;
font-weight: 600;
padding: 6px 12px 6px 8px;
cursor: pointer;
font-family: inherit;
backdrop-filter: blur(8px);
}
.back:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.hero-badge {
position: absolute;
top: 14px;
right: 14px;
background: var(--gold);
color: var(--ink);
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 5px 10px;
border-radius: 999px;
box-shadow: var(--shadow-1);
}
/* ── Body ── */
.card-body {
padding: 24px 24px 16px;
display: flex;
flex-direction: column;
gap: 18px;
}
.head-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.course {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--terracotta);
font-weight: 600;
margin-bottom: 4px;
}
.dish-name {
font-family: var(--font-display);
font-size: 1.7rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--ink);
line-height: 1.15;
}
.dish-price {
font-family: var(--font-mono);
font-size: 1.05rem;
font-weight: 700;
color: var(--terracotta-d);
white-space: nowrap;
padding-top: 4px;
}
.dish-desc {
font-size: 0.92rem;
color: var(--ink-2);
line-height: 1.55;
}
/* ── Chips ── */
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
list-style: none;
}
.chip {
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
background: var(--cream-2);
color: var(--ink-2);
}
.chip[data-tone="gf"] {
background: rgba(201, 168, 76, 0.2);
color: #8a7325;
}
.chip[data-tone="signature"] {
background: var(--gold);
color: var(--ink);
}
.chip[data-tone="warn"] {
background: rgba(217, 144, 32, 0.18);
color: #8a5a1a;
}
/* ── Modifier groups ── */
.group {
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: var(--r-md);
padding: 14px 14px 10px;
display: flex;
flex-direction: column;
gap: 6px;
background: var(--cream);
}
.group legend {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--ink-2);
padding: 0 6px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.req {
font-size: 0.62rem;
font-weight: 700;
background: var(--danger);
color: var(--bone);
padding: 2px 7px;
border-radius: 999px;
letter-spacing: 0.06em;
}
.hint {
font-size: 0.68rem;
font-weight: 500;
color: var(--warm-gray);
text-transform: none;
letter-spacing: 0;
}
.opt {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: var(--r-sm);
cursor: pointer;
font-size: 0.9rem;
color: var(--ink);
transition: background 0.15s;
}
.opt:hover {
background: var(--cream-2);
}
.opt input {
margin: 0;
accent-color: var(--forest);
width: 16px;
height: 16px;
flex-shrink: 0;
}
.opt-name {
flex: 1;
}
.opt-name em {
font-style: normal;
font-size: 0.72rem;
color: var(--terracotta);
margin-left: 6px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.opt-delta {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--warm-gray);
font-weight: 600;
}
.opt:has(input:checked) {
background: rgba(45, 74, 62, 0.08);
}
.opt:has(input:checked) .opt-name {
color: var(--forest-d);
font-weight: 600;
}
.opt:has(input:disabled) {
opacity: 0.45;
cursor: not-allowed;
}
/* ── Notes ── */
.notes-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.notes-field span {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--ink-2);
}
.notes-field textarea {
border: 1px solid rgba(44, 26, 14, 0.12);
background: var(--cream);
border-radius: var(--r-md);
padding: 10px 12px;
font-family: inherit;
font-size: 0.9rem;
color: var(--ink);
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
.notes-field textarea:focus {
border-color: var(--terracotta);
}
/* ── Footer ── */
.card-foot {
display: flex;
gap: 12px;
align-items: center;
padding: 16px 20px 20px;
background: var(--bone);
border-top: 1px solid rgba(44, 26, 14, 0.08);
position: sticky;
bottom: 0;
}
.qty {
display: inline-flex;
align-items: center;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 4px;
gap: 4px;
}
.qty-btn {
width: 32px;
height: 32px;
border-radius: 999px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
color: var(--ink);
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
font-family: inherit;
display: grid;
place-items: center;
line-height: 1;
}
.qty-btn:hover {
background: var(--cream-2);
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.qty-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.qty-num {
min-width: 28px;
text-align: center;
font-weight: 700;
font-size: 0.95rem;
font-variant-numeric: tabular-nums;
}
.add-btn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: var(--forest);
color: var(--bone);
border: none;
border-radius: 999px;
padding: 12px 20px;
font-family: inherit;
font-size: 0.92rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, transform 0.15s;
}
.add-btn:hover {
background: var(--forest-d);
}
.add-btn:active {
transform: scale(0.98);
}
.add-btn[disabled] {
background: var(--warm-gray);
cursor: not-allowed;
}
.add-total {
font-family: var(--font-mono);
font-weight: 700;
background: rgba(250, 247, 241, 0.18);
padding: 4px 10px;
border-radius: 999px;
font-size: 0.88rem;
}
/* ── 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%);
}
}
@media (max-width: 480px) {
.card-body {
padding: 18px 18px 12px;
}
.dish-name {
font-size: 1.45rem;
}
}const BASE_PRICE = 48;
const MAX_EXTRAS = 3;
const MAX_QTY = 10;
const qtyEl = document.getElementById("qty");
const totalEl = document.getElementById("total");
const addBtn = document.getElementById("add");
const toast = document.getElementById("toast");
const extras = document.querySelectorAll('fieldset[data-name="extras"] input[type="checkbox"]');
let qty = 1;
function readDelta() {
let delta = 0;
document.querySelectorAll("input[data-delta]:checked").forEach((input) => {
delta += Number(input.dataset.delta) || 0;
});
return delta;
}
function format(value) {
return `$${value.toFixed(2)}`;
}
function refresh() {
const unit = BASE_PRICE + readDelta();
totalEl.textContent = format(unit * qty);
// Enforce max extras
const checked = [...extras].filter((c) => c.checked);
extras.forEach((c) => {
c.disabled = !c.checked && checked.length >= MAX_EXTRAS;
});
// Qty button states
document.querySelectorAll(".qty-btn").forEach((btn) => {
const step = Number(btn.dataset.step);
if (step < 0) btn.disabled = qty <= 1;
if (step > 0) btn.disabled = qty >= MAX_QTY;
});
}
document.querySelectorAll(".qty-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const next = qty + Number(btn.dataset.step);
if (next < 1 || next > MAX_QTY) return;
qty = next;
qtyEl.textContent = qty;
refresh();
});
});
document.querySelectorAll("input[data-delta], .group input").forEach((input) => {
input.addEventListener("change", refresh);
});
addBtn.addEventListener("click", () => {
toast.hidden = false;
toast.textContent = `Added ${qty} × Ribeye 14oz · ${totalEl.textContent}`;
clearTimeout(addBtn._t);
addBtn._t = setTimeout(() => (toast.hidden = true), 2200);
});
refresh();<!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&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Menu Item Detail</title>
</head>
<body>
<article class="card" aria-label="Ribeye 14oz detail">
<header class="card-hero" role="img" aria-label="Plated ribeye">
<span class="hero-glyph">🥩</span>
<button class="back" type="button" aria-label="Back to menu">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path
d="M15 6l-6 6 6 6"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Back
</button>
<span class="hero-badge">Signature</span>
</header>
<div class="card-body">
<div class="head-row">
<div>
<p class="course">Principales</p>
<h1 class="dish-name">Ribeye 14oz</h1>
</div>
<span class="dish-price" id="basePrice">$48.00</span>
</div>
<p class="dish-desc">
Dry-aged 28 days, finished over olive wood. Served with bone marrow
butter, charred scallion, and house chimichurri.
</p>
<ul class="chips" aria-label="Dietary information">
<li class="chip" data-tone="gf">Gluten-free</li>
<li class="chip" data-tone="signature">Signature</li>
<li class="chip" data-tone="warn">Contains dairy</li>
</ul>
<fieldset class="group" data-type="single" data-name="doneness" data-required="true">
<legend>
Doneness <span class="req">Required</span>
</legend>
<label class="opt">
<input type="radio" name="doneness" value="Rare" />
<span class="opt-name">Rare</span>
<span class="opt-delta"></span>
</label>
<label class="opt">
<input type="radio" name="doneness" value="Medium rare" checked />
<span class="opt-name">Medium rare <em>chef's pick</em></span>
<span class="opt-delta"></span>
</label>
<label class="opt">
<input type="radio" name="doneness" value="Medium" />
<span class="opt-name">Medium</span>
<span class="opt-delta"></span>
</label>
<label class="opt">
<input type="radio" name="doneness" value="Well done" />
<span class="opt-name">Well done</span>
<span class="opt-delta"></span>
</label>
</fieldset>
<fieldset class="group" data-type="single" data-name="side">
<legend>Choose a side</legend>
<label class="opt">
<input type="radio" name="side" value="Mashed potato" data-delta="0" checked />
<span class="opt-name">Mashed potato</span>
<span class="opt-delta">Included</span>
</label>
<label class="opt">
<input type="radio" name="side" value="Garden salad" data-delta="0" />
<span class="opt-name">Garden salad</span>
<span class="opt-delta">Included</span>
</label>
<label class="opt">
<input type="radio" name="side" value="Truffle fries" data-delta="4" />
<span class="opt-name">Truffle fries</span>
<span class="opt-delta">+ $4.00</span>
</label>
<label class="opt">
<input type="radio" name="side" value="Grilled asparagus" data-delta="3" />
<span class="opt-name">Grilled asparagus</span>
<span class="opt-delta">+ $3.00</span>
</label>
</fieldset>
<fieldset class="group" data-type="multi" data-name="extras">
<legend>
Extras <span class="hint">Up to 3</span>
</legend>
<label class="opt">
<input type="checkbox" name="extras" value="Extra chimichurri" data-delta="2" />
<span class="opt-name">Extra chimichurri</span>
<span class="opt-delta">+ $2.00</span>
</label>
<label class="opt">
<input type="checkbox" name="extras" value="Bone marrow" data-delta="6" />
<span class="opt-name">Roasted bone marrow</span>
<span class="opt-delta">+ $6.00</span>
</label>
<label class="opt">
<input type="checkbox" name="extras" value="Truffle shave" data-delta="9" />
<span class="opt-name">Black truffle shave</span>
<span class="opt-delta">+ $9.00</span>
</label>
<label class="opt">
<input type="checkbox" name="extras" value="Cracked pepper" data-delta="0" />
<span class="opt-name">Cracked black pepper</span>
<span class="opt-delta">No charge</span>
</label>
</fieldset>
<label class="notes-field">
<span>Notes for the kitchen</span>
<textarea
id="notes"
rows="2"
maxlength="140"
placeholder="Allergies, preferences, occasion…"
></textarea>
</label>
</div>
<footer class="card-foot">
<div class="qty" aria-label="Quantity">
<button class="qty-btn" type="button" data-step="-1" aria-label="Decrease">−</button>
<span class="qty-num" id="qty">1</span>
<button class="qty-btn" type="button" data-step="1" aria-label="Increase">+</button>
</div>
<button class="add-btn" type="button" id="add">
<span>Add to order</span>
<span class="add-total" id="total">$48.00</span>
</button>
</footer>
</article>
<div class="toast" id="toast" hidden role="status" aria-live="polite">
Added to order
</div>
<script src="script.js"></script>
</body>
</html>Menu Item Detail
The detail surface that opens when a diner taps a dish in the carta. Shows a hero image area, description, allergen chips, a modifier picker (single-select size / doneness, multi-select extras with per-option price deltas), a special-instructions field, quantity stepper, and a sticky add-to-order CTA that reflects the live total.
Pairs with rest-menu-carta and rest-cart-order to form the customer ordering flow.