Build Guide — the recipe for the site itself
How this menu was plated
A chef's portfolio served as a seven-course tasting menu — every plate drawn in SVG, every course assembling itself as you scroll. This page is the recipe card: honest, specific, and written so another designer could cook it.
Concept & creative direction
The brief asked for a personal portfolio for a fictional Accra chef who cooks modern Ghanaian tasting menus. The founding decision: don't build a website about a tasting menu — build the tasting menu. The information architecture is the menu's own dramaturgy: amuse-bouche (hero), seven courses (the work), the chef (the person), private dining (the offer), notices (social proof), reservations (the ask).
Fine-dining print menus supplied the whole visual language: EB Garamond italics for dish names, small-caps course numerals, dot leaders running from dish to detail, hairline double-rule borders on the menu card. Charcoal #191817 is the dining room at night; rice paper #FBF6EE is the plate; saffron, palm green and pepper red only ever appear as food and accent — the palette is literally the plating palette.
Toolchain
- Fable 5 as designer-engineer, directed by Hannah Kwakye — concept, plating illustrations, copy and code developed together in iterative passes against screenshots.
- Hand-authored static HTML/CSS/JS. No frameworks, no build step, no libraries — scroll maths is ~40 lines of vanilla JS on
requestAnimationFrame, reveals are IntersectionObserver, everything else is CSS. - Self-hosted fonts: EB Garamond (roman + italic) and Instrument Sans as variable woff2 subsets, preloaded,
font-display: swap. Three files, ~120 KB total. - Playwright screenshots at mobile/tablet/desktop widths after every pass — the design critique loop ran on actual renders, not on wishful thinking.
Why every plate is drawn in code
The collection this site belongs to has a hard rule: no photographs, no AI-generated rasters — every visual authored in code. For a restaurant that constraint sounds fatal; it became the thesis. Food photography on chef sites is interchangeable. A top-view plate built from an SVG circle, thick round-capped sauce strokes, filled protein shapes and staggered garnish dots is not trying to imitate a photo — it is plating as diagram, the way chefs actually sketch dishes on paper during development.
Each dish keeps a strict grammar so the abstraction stays appetizing rather than clip-arty: one plate (radial-gradient circle plus two hairline rings), one sauce gesture, one protein mass, garnish as punctuation. Waakye risotto is a rust disc with rice-grain ellipses and a gari-tuile crescent; the octopus is three tapering round-capped strokes curling into a spiral with paper-coloured sucker dots. Nothing on any plate is more literal than it needs to be.
The signature interaction: scroll-plating
Each course is a tall section with a sticky, full-viewport scene pinned inside it. Scrolling through the section "cooks" the dish in stages.
1 — Progress per course
// section is 205vh tall; the pinned scene is 100vh
const travel = section.offsetHeight - innerHeight; // scroll budget
const p = clamp(-rect.top / travel, 0, 1); // 0 → 1 through the course
section.style.setProperty('--p', p);
One passive scroll listener, one rAF-throttled pass over all seven sections. The value of p also drives a subtle parallax — the plate drifts up 5vh as it plates, via translateY(calc((1 - var(--p)) * 5vh)).
2 — Assembly states, not tweens
Progress maps to five cumulative classes at fixed thresholds — plate .s1 (2%), sauce .s2 (18%), protein .s3 (40%), garnish .s4 (62%), steam .s5 (80%). All actual motion lives in CSS transitions keyed to those classes, so scrolling backwards un-plates the dish for free, and the browser compositor does the easing.
3 — The sauce sweep
<path class="draw" pathLength="1" d="M210,164 C148,198 …"
stroke-width="30" stroke-linecap="round"/>
.plate .draw { stroke-dasharray: 1; stroke-dashoffset: 1;
transition: stroke-dashoffset 1.3s cubic-bezier(.45,.05,.25,1); }
.course.s2 .sauce .draw { stroke-dashoffset: 0; }
pathLength="1" normalises every path to unit length, so one dash rule draws any sauce, any shape. A thick round-capped stroke drawing on looks exactly like a spoon dragging sauce across porcelain — the single most food-like trick on the site.
4 — Stagger through a custom property
<circle style="--i:3" …/> /* third garnish dot */
.plate .garnish > * { transform: scale(0);
transition: transform .6s cubic-bezier(.3,1.5,.45,1); /* overshoot = "pop" */
transition-delay: calc(var(--i, 0) * .07s); }
Every protein shape and garnish dot carries an index; garnish gets an overshoot curve so items land like they were placed with tweezers.
5 — Steam and chill
Hot courses (II–V) carry three wavy stroked paths that loop on a keyframe — rise 52px, fade in to 34% opacity, fade out — with per-path duration and delay via --sd/--so so the wisps never sync. The cold course (VI) inverts the idea: frost glints that twinkle instead of rise. Both are animation-play-state: paused whenever the course is off-screen, and both freeze to a faint static frame under reduced motion.
Menu typography as fine-dining art
The dot leader — the row of dots between a dish and its price — is done with a repeating radial gradient on a flex-grow spacer, so it stretches to any width without a single extra DOM node:
.leader .dots { flex: 1;
background-image: radial-gradient(circle, rgba(251,246,238,.42) 1px, transparent 1.4px);
background-size: 9px 3px; background-repeat: repeat-x;
background-position: 0 100%; height: .9em; }
The same component set the whole site's rhythm: menu-card rows, per-course ingredient lines, the chef's residency list, private-dining cards. Course numerals are letterspaced Instrument Sans small caps in saffron; dish names are EB Garamond italic at display size — the italic is a real cut, not a slant.
Provenance flip cards
Each course carries a small menu card that flips on hover, focus, or tap to reveal where the hero ingredient comes from. It is a real <button> with aria-expanded, two absolutely-stacked faces with backface-visibility: hidden, and a rotateY(180deg) on a perspective parent. The back face inverts to rice paper with charcoal text — turning the card literally turns the lights on. Under reduced motion the rotation is replaced by a 140ms crossfade.
Accessibility & reduced motion
- Under
prefers-reduced-motion: reducethe menu arrives fully plated: JS applies all five assembly states up-front, CSS forces every element visible with transitions killed, steam renders as a static faint wisp. The dish is never withheld from anyone. - Every plate SVG is
role="img"with a written description of the dish — the illustrations are content, not decoration. Steam and glints arearia-hidden. - Semantic landmarks, one
h1, skip link, visible:focus-visiblerings in saffron, keyboard-flippable provenance cards, AA-checked text colours (palm green and pepper red get lightened variants —#8FBF97,#E2694F— before they are ever used as small text on charcoal).
Performance
- Zero external requests; three preloaded font files; no libraries. All animation is
transform/opacity— no layout properties ever animate. - One scroll listener, rAF-throttled; courses outside a two-viewport window are skipped entirely; steam keyframes pause off-screen.
content-visibility: autoon the below-fold prose sections.
Iteration log
-
Pass 1 — design critique
- The brand mark's saffron arc plus two garnish dots accidentally composed a smiley face at 26px — redrew the mark (and favicon) as an off-centre quenelle-and-leaf cluster, and rotated Course I's brown-butter sweep off its symmetric "smile" axis for the same reason.
- Course III's gari tuile draped over the risotto read as blonde hair; re-cut it as a crescent shard leaning against the mound's right edge. Course IV's honey spiral was buried under the plantain coins — enlarged its radius to circle them and added two drizzle strokes over the coins at garnish stage, matching real plating order.
- Mobile: the amuse-bouche dot-leader line broke mid-word; it now stacks centred under 640px. The private-dining card leaders wrapped awkwardly ("twenty-four covers") — shortened and no-wrapped the terms.
- Densified Course VI's granita (a translucent base pool, eleven shards, ice-crystal dots) and gave Course V's guinea fowl grain-lines so the slices stopped reading as bread.
-
Pass 2 — elevation
- Gave every course a served-state finale: at 94% progress a saffron hairline ring blooms around the plate and the course's dot-leader lines ink in — the moment the dish is "sent".
- Upgraded the hero into a full mise-en-place moment: the amuse plate now assembles element by element on arrival, with a drifting steam wisp above it, so the signature interaction is taught before Course I.
- Sharpened the menu card with a double-rule frame and a centred fleuron; re-let-spaced the course numerals. Screenshot-worthy detail: the tracker rail numerals fill saffron as each course is plated.
-
Pass 3 — ship quality
- All three viewports re-shot for every route; zero console errors across / , /guide and /process; fonts verified loading from
assets/fonts. - Reduced-motion verified: all seven plates render fully composed, steam static, flip cards crossfade, no scroll-driven movement.
- Netlify form posts with honeypot and
?success=trueinline confirmation tested; every nav, footer, tracker and cross-route link resolved; copy proofread end to end.
- All three viewports re-shot for every route; zero console errors across / , /guide and /process; fonts verified loading from
Deploy pipeline
The site is fully static — index.html, one stylesheet, one script, three fonts — deployed on Netlify from the collection's repository, CI-driven on push. netlify.toml publishes the site root, sets immutable cache headers on /assets/* and conservative security headers everywhere. The reservations form is Netlify Forms with a honeypot field; no backend exists or is needed.
Chef Efua Mensah is a fictional persona and the Test Kitchen a fictional restaurant. The design thinking — and what was deliberately rejected — is documented in the design process.