Motion and interaction experiments, built for this site. Each entry is something you can see in action right here — and something I can explain line by line.
001
Preloader that becomes the headline
The intro greeting doesn't fade out — it flies into position as the first word of the hero headline, so loading and arrival read as one continuous sentence.
A GSAP timeline cycles greetings, then measures both the flying word and its landing slot with getBoundingClientRect — after document.fonts.ready, so glyph metrics are final. The word travels with a transform-only tween (x, y, scale) and is swapped for the real headline word the frame it lands. Plays once per session; skipped entirely under prefers-reduced-motion.
GSAPFLIP techniquefont loadingaccessibility
002
Lenis × ScrollTrigger as one system
Smooth scroll and scroll-triggered choreography share a single clock instead of fighting each other.
Lenis runs without its own rAF loop — gsap.ticker drives it, and Lenis' scroll events call ScrollTrigger.update. The nav scrollspy is four ScrollTriggers toggling active state, which replaced a hand-rolled scroll listener and a 1000 ms timing hack. When motion is reduced, Lenis is never created and everything falls back to native scrolling.
LenisScrollTriggerarchitectureperformance
003
A two-easing motion grammar
Every animation on this site uses one of two easing curves and four durations. Restraint is the system.
Entrances use power3.out, transitions use power2.inOut, and a bouncy back.out is reserved for small hover moments. Durations come from a four-step scale (0.3 / 0.6 / 0.9 / 1.4 s) and word reveals stagger at 0.06 s. The constants live in one file, so the site can't drift into having eleven different fade-ins. There are no idle loops — motion only responds to scroll or input.
motion designdesign tokensGSAP
004
Micro-interactions with cleanup
3D flip hovers and a scroll-reactive accent bar — all transform-only, all properly reverted.
The nav's flip hover is one reusable component instead of copy-pasted listeners. The hero's accent bar enters once, then couples to scroll position rather than looping idly. Everything registers through useGSAP scopes so unmounting reverts cleanly — no leaked ScrollTriggers, no orphaned listeners. Hover effects only attach on hover-capable devices.
ReactuseGSAPinteraction designcleanup
005
The dawn is the interface
The homepage opens at night and dawn breaks as you scroll to the work. Scroll position drives a whole-palette interpolation and a narrative Trollhättan clock.
The brand palette is three RGB-triplet CSS custom properties on :root, so a single ScrollTrigger can tween every token-coloured element at once by interpolating them. Night hands to warm day across the About section; dusk returns at the contact act, closing the loop the preloader opened. The status chip's clock is bound to the same scroll — 23:41 at the hero, 04:32 (real midsummer sunrise) as day breaks, back to 23:41 at the end — so the time is the narrator, not decoration. Reduced motion pins a static day theme and the real clock.
ScrollTriggerCSS variablesart directionstorytelling
006
Keeping contrast legible mid-fade
Fading text and background from dark to light naively meets at the same grey in the middle — 1:1 contrast. The dawn never dips below 4:1.
If both text and background interpolate linearly they cross at an identical mid-grey halfway through the transition, where the type disappears. Instead the background eases with a smoothstep — moving fast through the muddy middle values — while the text colour hard-swaps at 47% progress. Sampling contrast at every one-percent step of the scrub, the measured worst case across the whole dawn is 4.12:1, above the WCAG AA large-text bar. Accessibility survived a colour animation because the maths was checked, not eyeballed.
accessibilityWCAGcolouranimation