This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
I took a set of Apple’s interface principles — the WWDC “fluid interfaces” talks, translated to the web — and audited the whole site against them, then fixed the gaps. The interesting part wasn’t the motion polish. It was finding that a site built entirely on frosted glass honoured none of the accessibility settings that glass is supposed to respect.
I split the read across three passes — motion and interaction, materials and reduced-motion, typography and foundations — each producing findings tied to real files, not generic advice. The point of writing it down first was to separate the real defects from the things that were already fine, because a lot of it was already fine: reduced-motion is genuinely first-class here, the gallery-wall drag respects the grab offset, and no CSS animation is ever put on a grabbable element. The audit is what let me spend the fix budget on the gaps instead of re-touching good code.
This was the real finding. The app leans hard on translucent surfaces, and it responded to prefers-reduced-motion everywhere — but prefers-reduced-transparency and prefers-contrast were handled in exactly zero places. Someone who turns on Reduce Transparency because blur makes text hard to read got… all the blur, untouched.
prefers-reduced-transparency: reduce the glass and modal tokens go opaque, and the blur is stripped — including off the chrome that sets backdrop-filter as an inline style, where an !important author rule outranks a non-important inline declaration. That last detail is why a pure token swap wasn’t enough.prefers-contrast: more the faint borders (a lot of the UI is border-foreground/10) and muted text strengthen.prefers-reduced-motion: no-preference, because a brightness cross-fade is still motion, and the whole point is to respect that preference.A guard test reads the real CSS and fails if any of the three queries goes missing, so the next glass surface can’t quietly reintroduce the gap.
Apple’s first rule is that the moment lag appears, directness “falls off a cliff.” Every button in the app gave feedback only on hover and on click — nothing on the press itself. They now scale a hair on :active, reusing the design-system’s own timing tokens so the hover feel is unchanged, and dropping the transform entirely under reduced motion.
The shared modal was bouncing on entrance. A spring with overshoot feels right on something you flicked or threw; on a dialog that just faded in, it feels wrong. I added a critically-damped preset (spring.settle) and pointed the modal at it, and made its exit mirror its entrance. The header menu, which used to blink into existence, now springs out of its trigger’s corner — transform-origin: top right — so the spatial link between the button and the panel it opens is obvious.
Tracking should get tighter as display type grows; a 72px hero and a 30px section header shouldn’t share one letter-spacing value, but they did. Large headings now tighten with size. The display face is a variable font with an optical-size axis that was never being requested — so I asked for the opsz axis in the font load and turned on font-optical-sizing: auto, and the px font-size literals became rem so they scale when someone bumps their browser text size.
Two of the audit’s ideas I first left on the table — a route transition and a drag-to-dismiss sheet — because the naive versions each break something. A global fade fights the LCP work on the landing page (the hero paints in the server frame; starting it at opacity 0 delays exactly that), and a transform on a page wrapper turns every position: sticky header into something relative to the wrapper. So the route transition I shipped is opacity-only (sticky stays intact) and skips the very first paint of a session (LCP stays intact), fading only on later navigations, and not at all under reduced motion.
The sheet became a real primitive. It slides up, pairs with a scrim, and can be flicked away — and the dismiss decision uses Apple’s momentum-projection function, the same exponential-decay model iOS scroll uses: it projects where the flick would come to rest and lets go if that’s past the threshold, so a fast flick closes it even if it barely moved, while a gentle tug springs back. The project() helper is unit-tested and there’s a live demo in the motion lab. The lesson both share: the reason to defer wasn’t “too hard,” it was “the obvious version regresses something” — and once the guard is clear, they’re cheap.