The making of the front door
Build guide
This page teaches how the hub was built — honestly and specifically enough that another designer could reproduce every technique. It is the same guide every site in the collection carries, because showing the work is the work.
01 — Concept & creative directionAn index, not a carousel
The hub presents twenty-five finished sites. The temptation with a body of work that size is spectacle — a WebGL hall, a film reel, a scroll-jacked tour. We rejected all of it. A portfolio's job is to let a visitor find, judge, and visit the work, so the page is built as the most confident version of the most useful pattern there is: the editorial index. Numbered rows, one per project, set large in Instrument Serif — and behind it a second reading, the wall, where the same twenty-five projects hang as hand-drawn mini-posters.
The connective tissue is a thread. Every site in this collection was woven from the same constraint — every visual drawn in code — and the hub makes that literal: an ink thread weaves through the letters of the name in the hero, moss seams stitch the dark sections to the light ones, and each project row "floods" with its own site's palette, like dye taking to cloth.
Palette: ink #161513, porcelain #F5F3EF, electric lime-moss #A8B545, warm understory #E8E2D4. Type: Instrument Serif (display, with italic moments), Inter Tight (body/UI), Spline Sans Mono (labels). All three self-hosted as woff2 — four files, ~124 KB total, no external requests anywhere on the site.
Directed by Hannah, engineered with Fable 5
The whole collection was produced the same way: Hannah Kwakye directing — brief, palette, references, taste, edits — and Fable 5 (Anthropic's frontier model) working as the designer-engineer, writing every line of HTML, CSS, JavaScript, and SVG by hand. No frameworks, no build step, no CDN scripts, no npm dependencies in the shipped page. The stack is:
- Static, hand-authored HTML — one page plus
/guideand/processroutes. - One CSS file with custom properties as the design-token layer.
- One vanilla JS file (~12 KB unminified) — the thread canvas, the view toggle, row expansion, scroll reveals. IntersectionObserver and rAF cover everything; no library earns its bytes here.
- Netlify for hosting and the contact form; screenshot-driven QA with headless Chromium during the build.
Twenty-five posters, zero pixels of photography
The environment this collection was built in cannot download images — and that constraint became its thesis. Every project needed a thumbnail; instead of screenshots, each of the twenty-five got a hand-authored SVG mini-poster: a genuine miniature composition in that site's own palette and design language. The Makers poster weaves gold warp against ivory weft on emerald; Orvantis gets its aperture sigil in a field of silver particles; Voices of Ghana is three slabs of shouting type and a rubber stamp; HARMATTAN fades from haze to night with dust in between.
All twenty-five live in one inline <symbol> sprite and are referenced with <use> from both views — so each poster is authored once, rendered twice, and costs nothing extra on the wire. Posters average around forty SVG elements each; the whole set is ~45 KB of markup, less than a single JPEG thumbnail.
<symbol id="p-orvantis-intelligence" viewBox="0 0 240 320">
<rect width="240" height="320" fill="#07090D"/>
<circle cx="120" cy="140" r="34" stroke="#AEBECD"
stroke-dasharray="150 64"/> <!-- the aperture -->
...
</symbol>
<svg viewBox="0 0 240 320"><use href="#p-orvantis-intelligence"/></svg>
04 — The signature interaction
The ink thread, explained
The hero thread is a rope simulation drawn across two stacked canvases — one behind the headline, one in front. Because segments alternate between the layers, the thread genuinely passes behind the letterforms and re-emerges — it weaves through the name rather than sitting on top of it.
The rope
The rope is a fixed-distance chain: 140 points, each constrained to hang exactly 7px behind the one before it. The head eases toward a target; every follower snaps onto a circle of radius SEG around its leader. This is the entire physics engine:
head.x += (target.x - head.x) * 0.06;
head.y += (target.y - head.y) * 0.06;
for (let i = 1; i < N; i++) {
const dx = p[i].x - p[i-1].x, dy = p[i].y - p[i-1].y;
const d = Math.hypot(dx, dy) || 1;
p[i].x = p[i-1].x + (dx / d) * SEG; // stay SEG px behind
p[i].y = p[i-1].y + (dy / d) * SEG;
}
An earlier draft used plain easing (p[i] += (p[i-1] - p[i]) * k) — it collapsed the whole rope into a dot whenever the head slowed, because nothing preserved length. The distance constraint is what makes it read as thread.
The target
When your cursor is in the hero, the head chases it. Otherwise it ambles along a slow two-frequency Lissajous path centred on the headline, so the thread keeps writing calligraphy on its own. The cursor influence fades back to the wander over three seconds rather than snapping.
The weave
Each segment is assigned to the front or back canvas by a phase wave travelling along the rope: Math.sin(i * 0.42 − t * 2.1) > −0.15. Biasing the threshold keeps the thread mostly visible, dipping under the letters rather than hiding behind them. A thinner moss-coloured echo runs beside the ink line, offset along each segment's normal.
The ink trail
Neither canvas is ever cleared. Instead both fade each frame with globalCompositeOperation = 'destination-out' at 2% alpha — old strokes dissolve to transparency, so the thread leaves a calligraphic wash that reads as ink soaking into paper. On arrival the page "pre-rolls" five seconds of simulation cheaply (physics only, then one replay of the head's traced path with age-faded alpha) so the composition is already woven when you land — without blocking the main thread on 220 full canvas passes.
Rows that flood with their project's palette
Every row carries its site's palette as three custom properties, straight from the collection's briefs:
<li class="row" style="--pbg:#0A3B2E; --pfg:#F7F2E9; --pac:#E3B65C">
The flood is a ::before layer scaling from scaleY(0) at the bottom edge to full height on hover, focus, or expansion — transform-only, so it never causes layout. Text colours transition to --pfg/--pac at the same time, and the mini-poster peeks up out of the row, clipped by the row's own box.
Row expansion animates grid-template-rows: 0fr → 1fr — the modern height-auto animation, no JS measurement, driven by a real <button aria-expanded>. The Index/Wall toggle is two aria-pressed buttons with a sliding pill; the lists crossfade with a 330ms stagger, and under reduced motion they swap instantly.
The quiet requirements
- Reduced motion: the thread renders one pre-simulated static composition (no loop); reveals and the hero entrance are instant; the view toggle swaps without animation.
- Keyboard: skip link, visible
:focus-visiblerings, rows are real buttons, the flood also answers:focus-within. - Canvas discipline: device-pixel-ratio capped at 2; the rAF loop pauses when the hero scrolls off-screen (IntersectionObserver) and when the tab is hidden.
- Long-page cost: below-fold sections use
content-visibility: auto; all animation is transform/opacity only; fonts are preloaded withfont-display: swap. - Contrast: moss is decorative or paired with ink; text on every flood colour was chosen from that site's own palette pairs at AA or better.
Three passes, logged
-
Pass 1 — design critique
- The hero name was timid at 16.5vw with a dead right half — pushed to ~19.5vw so "Hannah / Kwakye" commits, and tightened hero padding so name + positioning both sit inside the first viewport.
- Found the thread collapsed to a single dot: the follower chain had no length constraint (and a variable-shadowing bug had silently replaced the rope's head with a DOM node). Rewrote as a fixed-distance chain.
- The hovering mini-poster overlapped the technique-tag column on desktop; repositioned it into the row's empty middle and dropped it below 1100px.
- Mobile kicker wrapped badly as flex columns; reset to inline flow.
-
Pass 2 — elevation
- Warm-up pre-roll added so the hero is already inked on arrival — first drafted as 220 synchronous canvas passes, which blocked the main thread for seconds under software rendering; replaced with a cheap physics-only simulation plus one age-faded replay of the head's path.
- Thread got its moss echo line, longer trail (2% fade), and a heavier taper so screenshots frame like calligraphy.
- Moss seam threads added at the top of the dark sections, drawing themselves in on scroll — the stitch that carries the motif past the hero.
- The Index/Wall toggle got a per-item cascade: each row and tile carries its number as a
--icustom property, staggering its entrance by 16ms — the wall now hangs itself picture by picture.
-
Pass 3 — ship quality
- A full-page capture exposed that
.wall { display: grid }was overriding thehiddenattribute — both views rendered stacked on load. Fixed with an explicit[hidden] { display: none }guard. - Every one of the 25 live links, both routes, the toggle, row expansion, and the form checked; zero console errors across mobile/tablet/desktop shots, in normal and reduced motion.
- Reduced-motion verified: static thread composition, instant reveals, no seam animation.
- Raised small-text contrast (tags, tile captions, meta lines) from ~40–60% ink to AA-passing values; proofread every word; confirmed fonts load and the page carries no external requests.
- A full-page capture exposed that
Shipping it
The site deploys to Netlify as static files — publish = ".", immutable cache headers on /assets/*, and conservative security headers on everything. The contact form is Netlify Forms with a honeypot field and a ?success=1 redirect handled inline. The whole collection deploys the same way, one site per Netlify project, cross-linked through the registry that generated the index above.
The point of all this: none of these techniques is secret. The constraint — draw everything in code — is a discipline anyone can adopt, and the model is an amplifier for whoever holds the pen. That is the collection's whole argument.