Skip to content
Alex Hollyman
Preview the style guide in a palette

Motion

How the animation is built, timed and switched off, using three easing curves, three durations and one switch.

The site animates a great deal, so the rules for it are strict. All of it is written by hand on a small toolkit in src/lib/motion. The only library is three.js, for the 3D scenes.

The switch

One switch in the header turns animation on or off for the whole site and remembers the choice. Until it is touched it follows the operating system's reduced-motion setting. It sets data-motion on the page before anything is drawn, and both the CSS and the scripts read that one attribute.

Easing

Three curves cover the site. Each is plotted here from the value in the stylesheet: time runs left to right, progress bottom to top.

  • ease-out

    Things arriving. Fast at first, then settling.

  • ease-in-out

    Things moving from one place to another.

  • ease-spring

    Anything a visitor has just let go of. Overshoots, then settles.

Code
<div class="motion:transition-transform duration-(--duration-base) ease-spring">…</div>

html[data-motion='full'] .badge {
  animation: sew var(--duration-slow) var(--ease-out) both;
}

Duration

Short for feedback, longer for distance. Scenes that run continuously keep their own time.

  • duration-quick 160ms Feedback: a switch, a hover, a press.
  • duration-base 320ms Most movement.
  • duration-slow 640ms Large distances and anything that should be watched.

A scene

Every canvas and WebGL scene follows one pattern: loaded when it nears the viewport, coloured from the palette around it, paused when off screen, and drawn once as a still when animation is off. This one is the smallest possible example. Change its palette with the menu above it, or move the pointer across it.

Code
import { whenNear } from '~/lib/motion';

whenNear(host, async () => {
  // three.js arrives with the scene, not with the page.
  const { mount } = await import('~/scenes/specimen');
  mount(host);
});

const loop = createLoop(host, {
  frame: (time, delta) => draw(time), // on screen, tab visible, animation on
  still: () => draw(0),               // animation off: one finished frame
});

Rules

Between pages

Moving between pages uses the browser's own view transitions between two ordinary HTML documents. The incoming page is drawn over the outgoing one from one side across, which shows the change of palette, and a shoal of fish in the new page's two inks swims across on top of the edge. The heading is picked at random each time: to the left or the right, level or on a diagonal. There is no client-side router. Browsers without view transitions load the next page as normal.