UI Components Medium
Modifier Sheet (Bottom Drawer)
Standalone modifier picker bottom sheet — required single-select, multi-select with caps, +/− with per-modifier deltas, special instructions and 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;
}
*,
*::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;
display: grid;
place-items: center;
padding: 32px 16px;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 420px;
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
gap: 12px;
}
.page-kicker {
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.dish-btn {
display: grid;
grid-template-columns: 64px 1fr auto;
align-items: center;
gap: 14px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 16px;
padding: 16px 18px;
cursor: pointer;
font-family: inherit;
color: var(--ink);
text-align: left;
box-shadow: 0 8px 24px rgba(44, 26, 14, 0.06);
}
.dish-btn:hover {
border-color: var(--terracotta);
}
.dish-glyph {
width: 64px;
height: 64px;
background: var(--cream-2);
border-radius: 12px;
display: grid;
place-items: center;
font-size: 2rem;
}
.dish-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.15rem;
}
.dish-desc {
font-size: 0.84rem;
color: var(--warm-gray);
}
.dish-price {
font-family: var(--font-mono);
font-weight: 700;
font-size: 1rem;
color: var(--terracotta-d);
}
.hint {
font-size: 0.84rem;
color: var(--warm-gray);
font-style: italic;
}
/* Backdrop */
.backdrop {
position: fixed;
inset: 0;
background: rgba(44, 26, 14, 0.5);
z-index: 10;
animation: fadeIn 0.18s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Sheet */
.sheet {
position: fixed;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: 100%;
max-width: 520px;
background: var(--bone);
border-radius: 20px 20px 0 0;
padding: 10px 0 0;
z-index: 20;
display: flex;
flex-direction: column;
max-height: 92vh;
box-shadow: 0 -20px 50px rgba(44, 26, 14, 0.2);
animation: sheetIn 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes sheetIn {
from {
transform: translate(-50%, 100%);
}
to {
transform: translate(-50%, 0);
}
}
.grip {
width: 36px;
height: 4px;
border-radius: 999px;
background: rgba(44, 26, 14, 0.18);
margin: 4px auto 8px;
}
.sheet-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 4px 20px 14px;
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
}
.kicker {
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
margin-bottom: 2px;
}
.sheet-head h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.4rem;
letter-spacing: -0.01em;
}
.close {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 999px;
width: 32px;
height: 32px;
display: grid;
place-items: center;
color: var(--ink-2);
cursor: pointer;
}
.close:hover {
background: var(--cream-2);
}
.sheet-body {
overflow-y: auto;
padding: 14px 20px 8px;
display: flex;
flex-direction: column;
gap: 14px;
}
.group {
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 12px;
padding: 14px 14px 10px;
background: var(--cream);
display: flex;
flex-direction: column;
gap: 4px;
}
.group legend {
font-size: 0.74rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-2);
font-weight: 700;
padding: 0 6px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.req {
font-size: 0.6rem;
background: var(--danger);
color: var(--bone);
padding: 2px 7px;
border-radius: 999px;
letter-spacing: 0.08em;
font-weight: 700;
}
.hint {
font-size: 0.68rem;
color: var(--warm-gray);
font-style: italic;
text-transform: none;
letter-spacing: 0;
font-weight: 500;
}
.hint b {
color: var(--ink);
}
.opt {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.92rem;
color: var(--ink);
transition: background 0.15s;
}
.opt:hover {
background: var(--cream-2);
}
.opt input {
margin: 0;
width: 16px;
height: 16px;
accent-color: var(--forest);
flex-shrink: 0;
}
.opt-name {
flex: 1;
}
.opt-name em {
font-style: normal;
font-size: 0.7rem;
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.1);
}
.opt:has(input:checked) .opt-name {
color: var(--forest-d);
font-weight: 700;
}
.opt:has(input:disabled) {
opacity: 0.45;
cursor: not-allowed;
}
.notes {
display: flex;
flex-direction: column;
gap: 6px;
}
.notes span {
font-size: 0.74rem;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 700;
color: var(--ink-2);
}
.notes textarea {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 10px;
padding: 10px 12px;
font-family: inherit;
font-size: 0.94rem;
color: var(--ink);
outline: none;
resize: vertical;
}
.notes textarea:focus {
border-color: var(--terracotta);
}
/* Foot */
.sheet-foot {
border-top: 1px solid rgba(44, 26, 14, 0.08);
padding: 12px 16px 16px;
background: var(--bone);
display: flex;
gap: 12px;
align-items: center;
}
.qty {
display: inline-flex;
align-items: center;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 3px;
gap: 4px;
}
.qty button {
width: 36px;
height: 36px;
border-radius: 999px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
color: var(--ink);
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
display: grid;
place-items: center;
line-height: 1;
font-family: inherit;
}
.qty button:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.qty button:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.qty span {
font-family: var(--font-mono);
font-weight: 700;
font-size: 1rem;
min-width: 28px;
text-align: center;
}
.add-btn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: var(--forest);
color: var(--bone);
border: none;
border-radius: 999px;
padding: 13px 20px;
font-family: inherit;
font-size: 0.94rem;
font-weight: 700;
cursor: pointer;
}
.add-btn:hover:not(:disabled) {
background: var(--forest-d);
}
.add-btn:disabled {
background: var(--warm-gray);
opacity: 0.7;
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.84rem;
}
/* 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: 0 12px 30px rgba(0, 0, 0, 0.18);
z-index: 30;
}
/* Visibility guard: honor the [hidden] attribute over base display */
.sheet[hidden] {
display: none;
}const BASE = 48;
const MAX_EXTRAS = 3;
const MAX_QTY = 10;
const sheet = document.getElementById("sheet");
const backdrop = document.getElementById("backdrop");
const addBtn = document.getElementById("add");
const totalEl = document.getElementById("total");
const qtyEl = document.getElementById("qty");
const hint = document.getElementById("hint");
const toast = document.getElementById("toast");
let qty = 1;
function open() {
sheet.hidden = false;
backdrop.hidden = false;
document.body.style.overflow = "hidden";
}
function close() {
sheet.hidden = true;
backdrop.hidden = true;
document.body.style.overflow = "";
}
document.getElementById("open").addEventListener("click", open);
document.getElementById("close").addEventListener("click", close);
backdrop.addEventListener("click", close);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !sheet.hidden) close();
});
function readDelta() {
let d = 0;
document.querySelectorAll("input[data-delta]:checked").forEach((i) => {
d += Number(i.dataset.delta) || 0;
});
return d;
}
function refresh() {
const unit = BASE + readDelta();
totalEl.textContent = `$${(unit * qty).toFixed(2)}`;
const extras = document.querySelectorAll('fieldset[data-name="extras"] input[type="checkbox"]');
const checked = [...extras].filter((c) => c.checked);
extras.forEach((c) => {
c.disabled = !c.checked && checked.length >= MAX_EXTRAS;
});
document.querySelectorAll("[data-step]").forEach((btn) => {
const s = Number(btn.dataset.step);
if (s < 0) btn.disabled = qty <= 1;
if (s > 0) btn.disabled = qty >= MAX_QTY;
});
// Required check: doneness must be selected to enable Add
const requiredOK = document.querySelector('fieldset[data-required="true"] input:checked');
addBtn.disabled = !requiredOK;
}
document.querySelectorAll("[data-step]").forEach((b) =>
b.addEventListener("click", () => {
const next = qty + Number(b.dataset.step);
if (next < 1 || next > MAX_QTY) return;
qty = next;
qtyEl.textContent = qty;
refresh();
})
);
document
.querySelectorAll(".group input, #notes")
.forEach((el) => el.addEventListener("change", refresh));
addBtn.addEventListener("click", () => {
const doneness = document.querySelector('input[name="doneness"]:checked')?.value || "—";
const side = document.querySelector('input[name="side"]:checked')?.value || "—";
const extras = [...document.querySelectorAll('input[name="extras"]:checked')].map((c) => c.value);
hint.innerHTML = `<strong>Added · ${qty} × Ribeye 14oz</strong> · ${doneness} · ${side}${extras.length ? ` · ${extras.join(" · ")}` : ""}`;
hint.style.color = "var(--forest-d)";
toast.textContent = `Added · ${totalEl.textContent}`;
toast.hidden = false;
clearTimeout(addBtn._t);
addBtn._t = setTimeout(() => (toast.hidden = true), 2200);
close();
});
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@700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Modifier sheet</title>
</head>
<body>
<main class="page">
<p class="page-kicker">Demo · click a dish to open the sheet</p>
<button class="dish-btn" type="button" id="open">
<span class="dish-glyph">🥩</span>
<span>
<p class="dish-name">Ribeye 14oz</p>
<p class="dish-desc">Marrow butter · chimichurri</p>
</span>
<span class="dish-price">$48</span>
</button>
<p class="hint" id="hint">No item added yet · open the sheet to build one.</p>
</main>
<div class="backdrop" id="backdrop" hidden></div>
<section class="sheet" id="sheet" hidden role="dialog" aria-modal="true" aria-labelledby="sheetTitle">
<div class="grip" aria-hidden="true"></div>
<header class="sheet-head">
<div>
<p class="kicker">Ribeye 14oz · base $48</p>
<h2 id="sheetTitle">Customise your dish</h2>
</div>
<button class="close" type="button" id="close" aria-label="Close">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</button>
</header>
<div class="sheet-body" id="body">
<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" data-delta="0" />
<span class="opt-name">Rare</span>
<span class="opt-delta"></span>
</label>
<label class="opt">
<input type="radio" name="doneness" value="Medium rare" data-delta="0" 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" data-delta="0" />
<span class="opt-name">Medium</span>
<span class="opt-delta"></span>
</label>
<label class="opt">
<input type="radio" name="doneness" value="Well done" data-delta="0" />
<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="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" data-max="3">
<legend>Extras <span class="hint">Up to <b id="maxLabel">3</b></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">
<span>Notes for the kitchen</span>
<textarea
id="notes"
rows="2"
maxlength="120"
placeholder="Allergies, preferences, occasion…"
></textarea>
</label>
</div>
<footer class="sheet-foot">
<div class="qty">
<button type="button" data-step="-1" aria-label="Decrease">−</button>
<span id="qty">1</span>
<button type="button" data-step="1" aria-label="Increase">+</button>
</div>
<button class="add-btn" type="button" id="add">
<span>Add to ticket</span>
<span class="add-total" id="total">$48.00</span>
</button>
</footer>
</section>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Modifier Sheet
The bottom-drawer modifier picker that opens when a server taps a dish on the POS or a diner taps a dish on the QR menu. Three group types: required single-select (Doneness), single-select (Side · with deltas), multi-select with cap (Extras · max 3). Special-instructions field, qty stepper, “Add to ticket” CTA shows live total. Drag handle, dim backdrop, Esc / backdrop click to close, focus trap.