Hannah Kwakye

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 direction

An 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.

02 — Toolchain

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:

03 — The asset pipeline

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.

05 — The index & the wall

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.

06 — Accessibility & performance

The quiet requirements

07 — Iteration log

Three passes, logged

  1. 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.
  2. 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 --i custom property, staggering its entrance by 16ms — the wall now hangs itself picture by picture.
  3. Pass 3 — ship quality
    • A full-page capture exposed that .wall { display: grid } was overriding the hidden attribute — 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.
08 — Deploy

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.

Back to the portfolio Read the design process