Skip to content

Motion

Motion is as quiet as the rest of the system. Colour and opacity change; things rarely move, and nothing scales, bounces or springs.

Four durations: --motion-fast for a tint change on a control, --motion-base for a surface change, --motion-slow for geometry such as a section folding, and --motion-entrance, which belongs to the dialog’s entrance and a change of theme and nothing else. Three curves: --ease-enter for things arriving, --ease-exit for things leaving, --ease-move for things changing size in place.

motion.css combines them into four transitions, --transition-tint, -surface, -fade and -collapse. Use those before writing a duration by hand.

TokenValueDescription
--motion-fastmotion.fast120msTint on a control: press, tab select, link.
--motion-basemotion.base160msSurfaces: nav solidify, dismiss, list item enter.
--motion-slowmotion.slow240msGeometry: height collapse, rail reflow.
--motion-entrancemotion.entrance350msThe dialog entrance and the colour fade of a theme change, nothing else.
--motion-shiftmotion.shift8pxThe only translate distance in the system.
--ease-enterease.entercubic-bezier(0, 0, 0.2, 1)Arriving or settling.
--ease-exitease.exitcubic-bezier(0.4, 0, 1, 1)Leaving.
--ease-moveease.movecubic-bezier(0.4, 0, 0.2, 1)Changing size or position in place.

Every set that changes uses three classes: .ds-reveal-in (fade in from 8px below), .ds-fade-out (opacity only) and .ds-collapse (height). A list that gains and loses members runs them in one order: leaving items fade out, the rest move, arriving items reveal in. --motion-shift is the only distance anything travels, and reveal-in is the only place it is used.

There is no scroll-triggered reveal, no parallax and no skeleton shimmer. A change of page fades with page transitions, which a site imports on its own. The one looping animation is the indeterminate progress bar, which always has a label.

The theme is one store per page, themeStore() from @rottensea/ds-personal-website/scripts/theme, and only the store changes it. It holds the reader’s choice (light, dark or system) and the theme in use, and it writes data-theme, the choice in localStorage, and the fade. It follows the OS while the choice is system, and other tabs of the site as they change the choice. ThemeToggle and SettingsMenu ask for a change with set(); SettingsMenu, Mermaid and SharePoster hear of one with subscribe(), so the panel always shows the current choice and a diagram draws again once for each real change.

import { themeStore } from "@rottensea/ds-personal-website/scripts/theme";
const theme = themeStore();
theme.set("dark");
theme.subscribe((state, previous) => {
if (state.theme !== previous.theme) redraw();
});

Every change fades every colour on the page together, over --motion-entrance with --ease-move. The store marks <html> with data-theme-fade for that time; while the mark is there, base.css gives every element the same colour transition in place of its own, so a button’s hover tint does not run at its own pace. What a script draws is not a CSS colour: Mermaid diagrams and SharePoster draw again in the new theme and fade the new drawing in over the old one with fadeIn, from the same module.

A script that writes data-theme itself is still taken as a change, faded and passed to the subscribers, with a warning in development. Its fade can come too late if it reads styles straight after writing, so set() is the way.

Motion drops to 1ms at two levels: the system’s prefers-reduced-motion setting, and a switch a site sets as data-reduce-motion="true" on <html>. Because either can be on at any time, motion only confirms an action; it never carries information of its own.

Two backgrounds keep moving under both: Starfield and DotGlobe. Their drift is slow and even, with nothing that flashes or jumps, and the site wants them alive.