Live
Kinetic Image Lab
Nine scroll-driven photographic studies: one scroll listener, every movement in CSS, pausable, and built from its own GitHub repository.

What it is
Nine photographic studies in scroll, space and perception. Each study takes one or more of my photographs of New York and moves them as you scroll: night cards drifting at different depths, a vertical scroll that becomes a lateral camera move along an avenue, a slow push into a sunset, six apertures sweeping across people in a doorway, one street shown empty during the pandemic and alive on a Christmas Eve years earlier, eight shutters assembling a protest from separate points of view, a march pulled into register from cyan, red and key layers, a sunrise built from sky, sun and buildings at separate depths, and one moment of a crowd repeating and receding.
The lab runs full page at the top of the photography page on this site, rendered from the repository's own source. A Pause motion button at the top right stops every animation, and the whole page honors the operating system's reduced motion setting.
The problem it addresses
A photography portfolio on the web usually shows photographs as flat tiles. Motion is what makes a long exposure or a layered sunrise feel like the moment it came from, but the usual ways to add motion carry costs: an animation library adds weight and lock-in, scroll-jacking fights the browser, and many effects ignore people who have asked their device for less motion.
This lab takes a different route. There is no animation library. One scroll listener measures how far each scene has travelled through the viewport and writes that as a number on the scene. Every movement after that is plain CSS reading that number. The browser keeps its own scrolling, the whole page can be paused, and reduced motion turns every scene into a still composition.
How it lives on this site
The repository on GitHub is the source, and it is an Astro project like this site. When the repository changes, one command, pnpm sync:kinetic, copies the latest commit's React component, stylesheet and images into this site's codebase, confines the stylesheet so it cannot restyle the rest of the site, and records the commit it came from (shown under the lab). The photography page then renders the lab as part of the site itself, with no iframe and no separate build. The result is committed and deployed by hand. Nothing watches the repository.
How it was made
A single React 19 component in an Astro 7 project, with Tailwind CSS 4 available (the styling itself is about 1,200 lines of hand-written CSS). The photographs come from my ImageKit portfolio, plus four stored in the repository.
What is involved
Each study is a tall section (between two and five screen heights) with a sticky stage inside it that fills the viewport. As the section scrolls through, the stage stays put and the content inside it moves.
The only JavaScript that runs during scrolling is one effect in the root component. On each scroll or resize it asks for one animation frame, and in that frame it reads every scene's bounding box, works out a local progress from 0 to 1 (how far the section's top has travelled above the viewport, divided by the section's height minus one screen), and writes that as two custom properties on the section: --p for progress and --inv for its inverse. It also sets the page progress bar.
Everything else is CSS. A card's transform is calc() over --p; a sky scales from 1.2 down as --p rises; the velocity track translates sideways by a multiple of --p; the registration layers slide from offset to aligned as --inv shrinks; the shutters open on a per-slice delay set with an --i index. Because the values are custom properties, each scene's CSS can be read on its own and tuned without touching the script.
Two switches keep it humane. The Pause motion button adds a class to the page that pauses every animation and pins --p at the midpoint, so each scene rests at a composed frame. A prefers-reduced-motion rule does the same without a click: scenes shrink to one screen height, transforms are removed, the moving layers are hidden and each study shows one still image.