Skip to content

Build log · Nocturne Studio

Brief

An after-hours agency reel. Aesthetic target is obys.agency and the late-night tier of Awwwards winners — giant kinetic typography, magnetic hover physics, image reveals that track the cursor, and a single blob cursor that morphs to snap onto interactive targets. The reviewer should open this chapter and feel the site pulling at them gently.

Patterns used

Decisions

The display is Anton, the body is Inter

Anton is ubiquitous on kinetic-type sites for a reason — it's narrow, high-contrast, and survives at 22vw without collapsing. Inter carries the prose where Anton would be shouting. The chapter tokens live on [data-chapter="studio"] and the palette is near-black paper (#0b0908) with a warm butter accent (#f6d67a), pulled to evoke film stock.

Magnetic buttons use gsap.quickTo, not per-frame tweens

gsap.quickTo builds a reusable tween and accepts a new target value on each call. On pointermove we dispatch the outer translate and the inner content translate separately, at different strengths (outer 0.35, inner 0.35 × 0.4). The inner parallax is what makes the button feel like a physical object — without it, the magnetic pull reads as a bug.

Line reveals use SplitType, not SplitText (GSAP)

GSAP's SplitText is paid. SplitType is ~4 KB free and does lines/words/chars just as well for this use. The pattern inserts an overflow: hidden wrapper around each generated line so the skew-up doesn't bleed into the lines above and below. The fallback (reduced-motion) is "don't split at all" — reveal instantly.

The kinetic type is scroll-scrubbed, not auto-scrolled

Auto-scrolling marquee exists on every agency site and is a solved problem (see our Editorial chapter's infinite-marquee-css). Scrubbing horizontal translate against vertical scroll is the distinct move — it ties typography to the reader's physical motion. gsap.fromTo with scrollTrigger.scrub: 1 is the whole implementation.

HoverRevealList and HoverSwapProjectIndex are two different patterns, on purpose

The reveal list's preview follows the cursor with a requestAnimationFrame loop lerped at 0.18 — it feels like a handheld lens. The hover-swap index's preview is sticky in a column and crossfades as different rows light up — more like an art book. Both answer "show me the image while I read" but with different physical metaphors. The chapter ships both to make the point.

The gooey cursor snaps to declared targets

Elements that want to attract the cursor carry data-gooey-target. On pointerenter the cursor grows to the element's bounding-box + 16px and rounds its radius to match. Applying the SVG <feGaussianBlur> + feColorMatrix filter gives it the metaball morph when crossing between nearby targets. The filter is defined inline in the component so it follows the cursor wherever it mounts.

Two cursors, no conflict

The Terminal chapter ships its own cursor (the site-wide custom-cursor-multi-state with a circle + text label). The Studio chapter replaces that with GooeyCursor. Both set pointer-events: none. To avoid double cursors, the Studio cursor uses mix-blend-mode: difference so it stays visible against both the chapter canvas AND the site-wide cursor that is still rendered from the root layout. In production I'd prefer to opt-out of the root cursor on chapters that ship their own; for a showcase, overlapping is fine and actually informative — you can see both render modes live next to each other.

Tradeoffs rejected

Performance notes

What I'd change in production

  1. Commission photography. Unsplash is a placeholder; a real studio's work is the entire pitch.
  2. Add a "first visit" tutorial (a single pointer pulse on the gooey cursor) so the interaction is legible to a reader who's never seen it before.
  3. Respect the OS accent color when available (color-scheme: light dark plus accentcolor queries) so Studio's butter accent harmonises with user preference instead of fighting it.
  4. Defer Inter's weight variants — we load the full family and use only two weights.