Cookbook — Ingredient list + check-off
A warm, editorial ingredient checklist for recipe pages. Items are grouped under sub-headings like for the rye crust, each row pairing a checkbox with quantity, unit, name and an optional prep note. Ticking an item strikes it through, updates a live X of Y gathered progress bar and per-group counts, while check-all and clear controls plus localStorage keep your mise en place exactly where you left it between visits.
MCP
Code
:root {
--cream: #faf6ef;
--paper: #fffdf8;
--ink: #2b2622;
--ink-2: #5c534a;
--muted: #8a7f73;
--tomato: #d6452b;
--tomato-d: #b8351e;
--saffron: #e8a33d;
--sage: #7c8a6b;
--clay: #c8775a;
--line: rgba(43, 38, 34, 0.12);
--line-2: rgba(43, 38, 34, 0.2);
--ok: #3f8f5f;
--warn: #d98a2b;
--danger: #c8412b;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--shadow-1: 0 1px 2px rgba(43, 38, 34, 0.1);
--shadow-2: 0 10px 30px rgba(43, 38, 34, 0.1);
--serif: "Fraunces", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.6;
color: var(--ink);
background:
radial-gradient(1200px 600px at 85% -10%, rgba(232, 163, 61, 0.1), transparent 60%),
radial-gradient(900px 500px at -10% 110%, rgba(214, 69, 43, 0.08), transparent 55%),
var(--cream);
min-height: 100vh;
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: clamp(1.5rem, 4vw, 3.5rem) clamp(1rem, 4vw, 2.5rem) 3rem;
}
/* ---------- Masthead ---------- */
.masthead {
text-align: center;
padding-bottom: 1.75rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--line);
}
.kicker {
margin: 0 0 0.65rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--tomato);
}
.title {
font-family: var(--serif);
font-weight: 600;
font-size: clamp(2rem, 5.2vw, 3.2rem);
line-height: 1.08;
letter-spacing: -0.01em;
margin: 0 auto 0.8rem;
max-width: 16ch;
}
.dek {
margin: 0 auto;
max-width: 52ch;
color: var(--ink-2);
font-size: 1.02rem;
}
.meta {
list-style: none;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
margin: 1.5rem 0 0;
padding: 0;
}
.meta li {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.55rem 1.1rem;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow-1);
min-width: 88px;
}
.meta-k {
font-size: 0.66rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.meta-v {
font-family: var(--serif);
font-weight: 600;
font-size: 1.05rem;
color: var(--ink);
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: clamp(1.25rem, 3vw, 2.25rem);
align-items: start;
}
/* ---------- Card ---------- */
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
padding: clamp(1.25rem, 3vw, 2rem);
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
}
.card-kicker {
margin: 0 0 0.15rem;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--sage);
}
.card-title {
font-family: var(--serif);
font-weight: 600;
font-size: 1.7rem;
margin: 0;
}
.emoji-frame {
display: grid;
place-items: center;
width: 52px;
height: 52px;
font-size: 1.5rem;
border-radius: 50%;
background: radial-gradient(circle at 30% 25%, #ffe9c4, #f3c98a);
border: 1px solid var(--line);
box-shadow: var(--shadow-1);
flex: none;
}
/* ---------- Progress ---------- */
.progress {
background: linear-gradient(180deg, rgba(232, 163, 61, 0.08), rgba(232, 163, 61, 0));
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0.9rem 1rem;
margin-bottom: 1.5rem;
}
.progress-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.progress-label {
margin: 0;
font-size: 0.95rem;
color: var(--ink-2);
}
.progress-label strong {
font-family: var(--serif);
font-size: 1.15rem;
color: var(--ink);
}
.progress-actions {
display: flex;
gap: 0.4rem;
}
.progress-track {
margin-top: 0.7rem;
height: 9px;
border-radius: 999px;
background: rgba(43, 38, 34, 0.08);
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--saffron), var(--tomato));
transition: width 0.35s ease;
}
.sr-status {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
/* ---------- Buttons ---------- */
.btn {
font-family: var(--sans);
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
border-radius: 999px;
padding: 0.4rem 0.9rem;
border: 1px solid var(--line-2);
background: var(--paper);
color: var(--ink);
transition: background 0.18s ease, border-color 0.18s ease, transform 0.08s ease;
}
.btn:hover {
background: #fff;
border-color: var(--clay);
color: var(--tomato-d);
}
.btn:active {
transform: translateY(1px);
}
.btn-ghost {
background: transparent;
}
/* ---------- Groups & items ---------- */
.groups,
.items {
list-style: none;
margin: 0;
padding: 0;
}
.group + .group {
margin-top: 1.5rem;
}
.group-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
padding-bottom: 0.5rem;
margin-bottom: 0.35rem;
border-bottom: 1px dashed var(--line-2);
}
.group-title {
font-family: var(--serif);
font-weight: 600;
font-size: 1.08rem;
margin: 0;
color: var(--tomato-d);
}
.group-count {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.item label {
display: grid;
grid-template-columns: auto 2.6rem auto 1fr;
align-items: baseline;
gap: 0.55rem;
padding: 0.5rem 0.4rem;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.15s ease;
}
.item label:hover {
background: rgba(232, 163, 61, 0.07);
}
.item input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
margin: 0;
align-self: center;
border: 1.5px solid var(--line-2);
border-radius: 6px;
background: var(--paper);
cursor: pointer;
position: relative;
transition: background 0.15s ease, border-color 0.15s ease;
flex: none;
}
.item input[type="checkbox"]:hover {
border-color: var(--clay);
}
.item input[type="checkbox"]:checked {
background: var(--sage);
border-color: var(--sage);
}
.item input[type="checkbox"]:checked::after {
content: "";
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid #fff;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg);
}
.item input[type="checkbox"]:focus-visible {
outline: 2px solid var(--tomato);
outline-offset: 2px;
}
.qty {
font-family: var(--serif);
font-weight: 600;
font-size: 1rem;
text-align: right;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.unit {
font-size: 0.82rem;
color: var(--muted);
white-space: nowrap;
}
.name {
color: var(--ink);
font-size: 0.97rem;
}
.note {
display: block;
font-size: 0.78rem;
color: var(--muted);
font-style: italic;
line-height: 1.35;
}
.item.is-checked label {
background: rgba(124, 138, 107, 0.08);
}
.item.is-checked .qty,
.item.is-checked .name {
text-decoration: line-through;
text-decoration-color: var(--muted);
color: var(--muted);
}
.item.is-checked .note {
text-decoration: line-through;
}
/* ---------- Aside ---------- */
.aside {
position: sticky;
top: 1.5rem;
}
.photo {
margin: 0;
position: relative;
aspect-ratio: 4 / 3;
border-radius: var(--r-lg);
overflow: hidden;
border: 1px solid var(--line);
box-shadow: var(--shadow-2);
}
.photo-art {
position: absolute;
inset: 0;
}
.photo-art-1 {
background:
radial-gradient(120% 90% at 50% 120%, #8a3a1e 0%, transparent 55%),
linear-gradient(160deg, #f4d6a0 0%, #e8a33d 38%, #d6452b 100%);
}
.photo-blob {
position: absolute;
border-radius: 50%;
filter: blur(0.5px);
box-shadow: inset -6px -8px 14px rgba(0, 0, 0, 0.18), inset 4px 5px 10px rgba(255, 255, 255, 0.3);
}
.blob-1 {
width: 38%;
height: 38%;
left: 12%;
top: 26%;
background: radial-gradient(circle at 35% 30%, #ff7a52, #c8311a);
}
.blob-2 {
width: 30%;
height: 30%;
right: 14%;
top: 18%;
background: radial-gradient(circle at 35% 30%, #ff9d5e, #d6452b);
}
.blob-3 {
width: 26%;
height: 26%;
left: 42%;
top: 52%;
background: radial-gradient(circle at 40% 35%, #ffd27a, #e8a33d);
}
.photo-emoji {
position: absolute;
right: 0.9rem;
bottom: 0.7rem;
font-size: 2rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25));
}
.photo-cap {
margin: 0.7rem 0 0;
font-size: 0.85rem;
font-style: italic;
color: var(--muted);
text-align: center;
}
.tip {
margin-top: 1.5rem;
background: var(--paper);
border: 1px solid var(--line);
border-left: 3px solid var(--sage);
border-radius: var(--r-md);
padding: 1rem 1.1rem;
box-shadow: var(--shadow-1);
}
.tip-kicker {
margin: 0 0 0.3rem;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--sage);
}
.tip-body {
margin: 0;
font-size: 0.92rem;
color: var(--ink-2);
}
/* ---------- Footer ---------- */
.foot {
margin-top: 2.5rem;
padding-top: 1.25rem;
border-top: 1px solid var(--line);
text-align: center;
font-size: 0.83rem;
color: var(--muted);
}
.dot {
margin: 0 0.4rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translate(-50%, 1.5rem);
background: var(--ink);
color: var(--paper);
font-size: 0.88rem;
font-weight: 500;
padding: 0.7rem 1.2rem;
border-radius: 999px;
box-shadow: var(--shadow-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.layout {
grid-template-columns: 1fr;
}
.aside {
position: static;
order: -1;
}
.photo {
aspect-ratio: 16 / 9;
}
}
@media (max-width: 420px) {
.item label {
grid-template-columns: auto 2.2rem auto 1fr;
gap: 0.45rem;
}
}
@media print {
body {
background: #fff;
}
.progress-actions,
.aside,
.toast,
.foot {
display: none;
}
.card {
box-shadow: none;
border-color: #999;
}
}(function () {
"use strict";
var STORAGE_KEY = "cookbook:recipe-ingredient-list:galette-14";
var groupsRoot = document.getElementById("groups");
var checkboxes = Array.prototype.slice.call(
groupsRoot.querySelectorAll('input[type="checkbox"]')
);
var gatheredCount = document.getElementById("gathered-count");
var totalCount = document.getElementById("total-count");
var fill = document.getElementById("progress-fill");
var track = fill.parentElement;
var liveStatus = document.getElementById("live-status");
var checkAllBtn = document.getElementById("check-all");
var clearAllBtn = document.getElementById("clear-all");
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2000);
}
// Give each checkbox a stable index-based id for persistence.
checkboxes.forEach(function (cb, i) {
cb.dataset.idx = String(i);
});
function save() {
var checked = checkboxes
.map(function (cb, i) {
return cb.checked ? i : -1;
})
.filter(function (i) {
return i >= 0;
});
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(checked));
} catch (e) {
/* storage unavailable — ignore */
}
}
function load() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
var checked = JSON.parse(raw);
if (!Array.isArray(checked)) return;
checked.forEach(function (i) {
if (checkboxes[i]) checkboxes[i].checked = true;
});
} catch (e) {
/* malformed — ignore */
}
}
function updateGroups() {
var groups = groupsRoot.querySelectorAll(".group");
groups.forEach(function (group) {
var boxes = group.querySelectorAll('input[type="checkbox"]');
var done = 0;
boxes.forEach(function (b) {
if (b.checked) done++;
});
var label = group.querySelector("[data-group-count]");
if (label) label.textContent = done + "/" + boxes.length;
});
}
function updateItemStates() {
checkboxes.forEach(function (cb) {
var item = cb.closest(".item");
if (item) item.classList.toggle("is-checked", cb.checked);
});
}
function render() {
var total = checkboxes.length;
var done = checkboxes.filter(function (cb) {
return cb.checked;
}).length;
var pct = total ? Math.round((done / total) * 100) : 0;
gatheredCount.textContent = String(done);
totalCount.textContent = String(total);
fill.style.width = pct + "%";
track.setAttribute("aria-valuenow", String(pct));
liveStatus.textContent = done + " of " + total + " ingredients gathered";
updateItemStates();
updateGroups();
}
groupsRoot.addEventListener("change", function (e) {
if (e.target && e.target.matches('input[type="checkbox"]')) {
render();
save();
}
});
checkAllBtn.addEventListener("click", function () {
checkboxes.forEach(function (cb) {
cb.checked = true;
});
render();
save();
toast("All ingredients gathered 🧺");
});
clearAllBtn.addEventListener("click", function () {
checkboxes.forEach(function (cb) {
cb.checked = false;
});
render();
save();
toast("List cleared");
});
load();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cookbook — Ingredient list + check-off</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead" role="banner">
<p class="kicker">Sunday Suppers · No. 14</p>
<h1 class="title">Roasted Tomato & Saffron Galette</h1>
<p class="dek">A free-form tart with blistered cherry tomatoes, caramelised shallots and a whisper of saffron, folded into a buttery rye crust.</p>
<ul class="meta" aria-label="Recipe facts">
<li><span class="meta-k">Serves</span><span class="meta-v">6</span></li>
<li><span class="meta-k">Active</span><span class="meta-v">35 min</span></li>
<li><span class="meta-k">Total</span><span class="meta-v">1 hr 20</span></li>
<li><span class="meta-k">Level</span><span class="meta-v">Easy</span></li>
</ul>
</header>
<main class="layout" role="main">
<article class="card" aria-labelledby="ing-heading">
<div class="card-head">
<div>
<p class="card-kicker">Mise en place</p>
<h2 id="ing-heading" class="card-title">Ingredients</h2>
</div>
<span class="emoji-frame" aria-hidden="true">🍅</span>
</div>
<div class="progress" aria-hidden="false">
<div class="progress-row">
<p class="progress-label"><strong id="gathered-count">0</strong> of <span id="total-count">0</span> gathered</p>
<div class="progress-actions">
<button type="button" class="btn btn-ghost" id="check-all">Check all</button>
<button type="button" class="btn btn-ghost" id="clear-all">Clear</button>
</div>
</div>
<div class="progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Ingredients gathered">
<div class="progress-fill" id="progress-fill"></div>
</div>
<p class="sr-status" id="live-status" aria-live="polite"></p>
</div>
<ul class="groups" id="groups">
<li class="group" data-group="Pastry">
<div class="group-head">
<h3 class="group-title">For the rye crust</h3>
<span class="group-count" data-group-count>0/0</span>
</div>
<ul class="items">
<li class="item"><label><input type="checkbox" /><span class="qty">200</span><span class="unit">g</span><span class="name">rye flour</span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">120</span><span class="unit">g</span><span class="name">cold unsalted butter<small class="note">cubed, chilled</small></span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">1</span><span class="unit">tsp</span><span class="name">flaky sea salt</span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">60</span><span class="unit">ml</span><span class="name">ice water<small class="note">add a splash at a time</small></span></label></li>
</ul>
</li>
<li class="group" data-group="Filling">
<div class="group-head">
<h3 class="group-title">For the saffron filling</h3>
<span class="group-count" data-group-count>0/0</span>
</div>
<ul class="items">
<li class="item"><label><input type="checkbox" /><span class="qty">450</span><span class="unit">g</span><span class="name">mixed cherry tomatoes 🍅</span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">3</span><span class="unit"></span><span class="name">banana shallots<small class="note">thinly sliced</small></span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">2</span><span class="unit">cloves</span><span class="name">garlic 🧄</span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">1</span><span class="unit">pinch</span><span class="name">saffron threads<small class="note">bloomed in warm water</small></span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">120</span><span class="unit">g</span><span class="name">soft goat’s cheese</span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">2</span><span class="unit">tbsp</span><span class="name">extra-virgin olive oil</span></label></li>
</ul>
</li>
<li class="group" data-group="Finish">
<div class="group-head">
<h3 class="group-title">To finish</h3>
<span class="group-count" data-group-count>0/0</span>
</div>
<ul class="items">
<li class="item"><label><input type="checkbox" /><span class="qty">1</span><span class="unit"></span><span class="name">egg<small class="note">beaten, for the wash</small></span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">1</span><span class="unit">handful</span><span class="name">fresh basil & thyme 🌿</span></label></li>
<li class="item"><label><input type="checkbox" /><span class="qty">1</span><span class="unit">tsp</span><span class="name">lemon zest 🍋</span></label></li>
</ul>
</li>
</ul>
</article>
<aside class="aside" aria-label="Recipe photo and tip">
<figure class="photo" aria-hidden="true">
<div class="photo-art photo-art-1"></div>
<div class="photo-blob blob-1"></div>
<div class="photo-blob blob-2"></div>
<div class="photo-blob blob-3"></div>
<span class="photo-emoji">🥧</span>
</figure>
<figcaption class="photo-cap">Blistered, jammy and golden at the edges.</figcaption>
<div class="tip">
<p class="tip-kicker">Cook’s note</p>
<p class="tip-body">Salt the tomatoes 15 minutes ahead and pat them dry — a drier filling keeps the rye crust crisp instead of soggy.</p>
</div>
</aside>
</main>
<footer class="foot" role="contentinfo">
<p>Your progress is saved on this device. <span class="dot" aria-hidden="true">·</span> Illustrative UI — recipe is fictional.</p>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Ingredient list + check-off
A print-friendly mise en place panel for any recipe page. Ingredients are organised into labelled groups (for the rye crust, for the saffron filling, to finish), and each line is a single accessible label wrapping a checkbox, a serif quantity, its unit, the ingredient name and an optional italic prep note. Tick anything and it strikes through with a soft sage highlight.
A live progress bar at the top reports X of Y gathered and fills from saffron to tomato as you
work, while every group shows its own done/total tally. Check all gathers the whole list at once,
Clear resets it, and an aria-live region announces each change for screen readers. Focus-visible
rings, tabular numerals and a 720px single-column breakpoint keep it usable from phone to desktop,
and @media print drops the controls for a clean shopping list.
State is persisted to localStorage keyed per recipe, so a half-gathered list survives a reload or
a return trip from the pantry — no backend, no build step, just one HTML file, one stylesheet and a
small vanilla-JS module.
Illustrative UI only — recipes & nutrition data are fictional, not dietary advice.