Loop & Larder is a warm light-theme storefront for a fictional coffee roastery, built with Astro and exactly one photograph. The headline is Playfair Display; the human touches — the "fresh this week" script, the arrows, the roast meters — are SVG ink that draws itself on as you scroll; and the subscription builder is a small vanilla-JS island that keeps a running total honest in real time.
A specialty roastery sells two things at once: a product (this week's four single origins, each with an origin, an elevation, a process and a roast level) and a habit (a subscription you trust enough to leave running). So the site is an editorial magazine page and a commerce configurator wearing the same clothes. The voice is warm and specific — "Roasted Tuesday. At your door Thursday. Gone by Sunday." — and the design keeps a crisp editorial grid, then lets a hand break into it with ink: a circled note, a drawn arrow, a signature. The commerce patterns underneath are borrowed from shipped DTC apps (Blue Apron's live recurring total, Walmart's subscribe-vs-one-time cards, Cleo's "save 15%" framing) and dressed in the roastery's own hand.
.astro components: reusable at author time, free at run time. A
growing catalog would graduate the coffee data into Astro content collections without touching a
single template.The "fresh this week" script, the little arrows, the underlines and the roastery signature are all
hand-written SVG paths. Each visible stroke carries pathLength="1", which normalizes
its length to 1 so CSS can animate the draw without JavaScript ever measuring anything:
<path class="p" pathLength="1" d="M31 30 C25 25 17 28 17 39 L17 65"/>
.js .ink .p { stroke-dasharray: 1; stroke-dashoffset: 1; }
.js .ink.is-in .p { stroke-dashoffset: 0;
transition: stroke-dashoffset 1.6s var(--ease); } A single IntersectionObserver adds .is-in when a drawing scrolls into
view, and each letter's stroke draws in sequence — so "fresh" appears the way a hand would write
it. The same observer fills the five-dot roast meters and runs the stat counters.
The easing is the site's signature cubic-bezier(.2,.7,.15,1) — "the pour": a slow tip,
then a smooth stream. Under prefers-reduced-motion the dashes are stripped and every
stroke, dot and number simply arrives final.
The builder is three radio groups — coffee, grind, cadence — plus a subscribe/one-time toggle,
with a dark summary card that recomputes on every change. The groups are real <input type="radio"> elements (visually hidden, fully keyboard-operable with
arrow keys); the selected-card look and focus ring come from CSS :has(). All the logic
is one function that reads the checked inputs and writes the summary:
function update() {
const subscribe = checked('mode').value === 'subscribe';
const base = parseFloat(checked('origin').dataset.price);
const perBag = subscribe ? base * 0.85 : base; // subscribe = 15% off
const per = parseInt(checked('cadence').dataset.per, 10); // 4 / 2 / 1 per month
if (subscribe) {
total.textContent = money(perBag * per); // live monthly total
saveText.textContent = `${money(perBag)} a bag — you save ${money(base-perBag)}`;
} else {
total.textContent = money(perBag); // one bag, ships once
cadenceStep.classList.add('dim'); // cadence not relevant
}
} Why it reads as trustworthy: shipped subscription apps never hide the math.
The summary restates every choice in plain language and shows the per-bag price and the
monthly total, so there's no surprise at a checkout that, in this demo, doesn't exist. The whole
card lives inside an aria-live="polite" region, so screen-reader users hear the
total change as they build.
feTurbulence tile,
inlined as a data URI, tinted warm umber by a feColorMatrix and fixed over the page
with mix-blend-mode: multiply. No image request, no canvas — noise the browser
rasterizes once.--copper is reserved for large display accents and ink; any small copper text or link
uses --copper-deep (5.1:1). Umber body text on cream is 10.2:1, ink is 13.5:1..js class gates every reveal and the
draw-on. With JavaScript off, the page renders complete — headline, cards, roast meters and the
full builder markup all present; only the animation is skipped.aria-label ("Roast level 2 of 5 — Light"), so the spec is
readable without seeing the dots.Astro builds static HTML, so Pages serves it straight from the repo — no Actions workflow needed. Three config lines do the work:
// astro.config.mjs
export default defineConfig({
site: 'https://bswxyz.github.io',
base: '/loop-and-larder', // project-site path prefix
outDir: './docs', // Pages serves main + /docs
}); Every internal link and asset routes through import.meta.env.BASE_URL so nothing 404s
under the /loop-and-larder/ prefix, a .nojekyll in public/
stops Pages from mangling the build, and deploying is:
npm run build # emits ./docs
git add -A && git commit -m "batch LL-118"
gh repo create bswxyz/loop-and-larder --public --source . --push
# then: Pages → main + /docs Loop & Larder is a design-showcase concept — the coffees, farms, prices and numbers are fictional, and there is no cart, checkout or subscription backend. The repository README maps exactly what a production version would need.
← Back to the roastery