Reconstruct an interface from measurement, not screenshots

A screenshot tells you what something looked like for one frame. Measuring the live page tells you what it does — and only one of those is enough to rebuild from.

Rebuilding an interface you admire is a normal way to learn. The usual method is to open a screenshot, eyeball the spacing, and iterate until it looks close. That method has a ceiling, and the ceiling is lower than most people expect.

A screenshot records one frame. Interfaces are not one frame. The thing that makes a good interface feel expensive is almost always in the parts a screenshot cannot hold: how fast something responds, what it does on the way out, which of two neighbouring properties moves first. You can match a screenshot perfectly and still produce something that feels wrong the moment a pointer touches it.

The alternative is to measure. Drive the real page with a browser automation tool, read computed styles and geometry while you move the pointer and the scroll position, and write the numbers down before writing any code.

What a screenshot cannot tell you

Three categories, roughly.

Timing. A hover that takes 300ms feels different from one that takes 600ms, and a screenshot of the settled state is identical for both. Worse, the ratio between two properties matters more than either duration: a border that answers in 300ms while an image drifts for 600ms reads as considered, and the same two properties at 320ms and 520ms read as flat. That is a 60ms difference that no static comparison will ever surface.

Direction of travel. Two elements can end at the same place having got there in opposite ways. A card that shrinks and then dissolves is one object receding. A card that dissolves while it is still shrinking is two cards swapping. Same start, same end, completely different feel.

What is not there. Measuring tells you when a property does not move, which is often the more useful finding. If the image scales on hover but the card does not, that is a decision, and copying it means resisting the urge to add a lift.

The method

Load the page under automation, then sample.

const styles = await page.evaluate(() => {
  const el = document.querySelector('.target');
  const cs = getComputedStyle(el);
  return { transform: cs.transform, opacity: cs.opacity, transition: cs.transition };
});

That much is obvious. The parts that are not obvious:

Sample per frame, not per event. A single before/after reading gives you the endpoints and nothing about the curve. Install a requestAnimationFrame sampler in the page before you trigger the interaction, and you get the whole ramp.

Hold the page completely still. This is the one that catches people. If you sample while scrolling, a time-based animation and a scroll-linked one produce identical data, because your scroll steps and your time steps are both constant. The only way to tell them apart is to stop scrolling and keep reading. A ticker that keeps moving with the page motionless is time-based; one that freezes is scroll-linked. Getting this backwards means building something that looks right while you scroll and is dead the moment you stop.

Sample finely enough to see a plateau. Coarse sampling smooths over exactly the detail that carries the feel. At 100px scroll intervals a two-phase transition looks like one curve. At 25px the plateau in the middle is obvious, and a plateau is something no single easing function can produce — so its presence tells you the effect has two stages.

Fit a curve, then say what you fitted

Once you have a ramp, normalise it and compare it against candidates.

Take the measured values, express them as a fraction of the total travel, and check them against the standard easings at the same progress. If the numbers sit within a percent or two of 1 − (1 − p)³ across the whole range, you have an ease-out cubic, and you can say so with a table rather than a feeling.

Then write down what you could not distinguish. An ease-out cubic and a critically-damped spring fit most ramps about equally well at 60fps sampling. If your data cannot separate them, the honest note is that your data cannot separate them — not a confident claim about which one it is.

Copying numbers is not copying code

Worth being precise about, because it is easy to get uncomfortable here.

Measuring a page tells you observable facts: this transition takes 300ms, this scale is 1.05, this element does not move. Those are properties of the rendered result, the same way the tempo of a song is a property of the recording. Reproducing a behaviour you measured, in your own markup, with your own tokens, is what every implementation of a known interaction pattern does.

What you must not do is take the artefacts: the source, the stylesheet, the class names, the copy, the assets, the specific bezier constants lifted out of someone else's file. The distinction is not subtle in practice. If you find yourself copying a value you could not have derived from observation, stop.

The frame budget will bite you

One measurement that changes an implementation more than any easing choice: how many frames you actually get.

An ease-out is steepest at its very first frame. At 16ms per frame that first step is small enough to be invisible. At 45ms it is three times larger, and the whole ramp reads as a snap followed by a glide. The curve is not wrong; the budget is different.

So measure your own frame interval too, not just the reference's. If yours is slower, matching the reference's curve exactly will not match its feel. Either find the cost and remove it, or pick a curve with a lower peak slope and distribute the movement more evenly. Both are legitimate. Pretending the frame rates are the same is not.

What you end up with

A note, before any code, that reads like a specification: the properties that move, the ones that do not, the distances, the durations, the curve fits with their residuals, and an explicit list of what you could not determine.

That note is the deliverable. The implementation that follows it is almost mechanical, and — more usefully — every value in it can be defended six months later, because it came with its measurement attached.

All articles