
Motion on a website is four numbers per element: a distance, a duration, a curve, and a delay between siblings. Every brief that says smooth, fluid or buttery has supplied none of the four, so a model supplies them from the framework, and the framework has one answer.
That answer is cubic-bezier(0.4, 0, 0.2, 1) at 150ms, applied to everything. It is a fine curve for a button and the wrong curve for a headline, and it appears on both because nothing distinguished them.
Name the four numbers and the page moves the way you meant. The table below is the shortest version of a motion brief that actually works.
Four numbers, one curve, one refusal#
A motion specification has two halves. The first half is per element and mechanical. The second half is per page and editorial, and it is the half that almost never gets written down.
Per element, the four numbers are always the same four:
- Distance. How far the thing travels, in px or percent.
24pxfor a headline line,16pxfor body copy,0for anything that only fades. - Duration. In milliseconds, and different per element size. A larger object needs longer to look like it has mass.
- Curve. Written as its
cubic-bezier()value, not as a word. Ease out is a family, not a value, and the members of the family look different. - Stagger. The gap between siblings, in seconds.
0.06reads as one gesture,0.14reads as a list being dealt out,0.3reads as slow.
Per page, there is one more decision and it carries more weight than any of the four: the curve you will reuse everywhere. A page with one entrance curve reads as designed. A page with five reads as assembled, and readers notice without being able to name what they noticed.
Then the refusal. Decide what does not move, and write it into the brief as a line of its own. Body paragraphs, form labels, navigation, footers, anything below the second screen: most of a good page is still. A page where everything animates on entry is the single most reported tell of a generated site, and the fix is a sentence, not a technique.
The motion table#
This is the whole of a page's motion, at values you can paste into any model. Change the numbers, keep the shape.
| Element | Distance | Duration | Curve | Stagger |
|---|---|---|---|---|
| Hero headline, per line | 28px up, mask reveal | 680ms | cubic-bezier(0.22, 1, 0.36, 1) | 0.08 |
| Hero subhead | 18px up | 540ms | cubic-bezier(0.22, 1, 0.36, 1) | after headline, 0.12 delay |
| Primary button | 0, opacity only | 420ms | cubic-bezier(0.22, 1, 0.36, 1) | 0.2 delay |
| Button hover | 2px lift | 140ms | cubic-bezier(0.4, 0, 0.2, 1) | none |
| Section heading | 24px up | 600ms | cubic-bezier(0.22, 1, 0.36, 1) | none |
| Card grid | 20px up | 520ms | cubic-bezier(0.25, 1, 0.5, 1) | 0.06 |
| Body copy | none | none | none | none |
| Image reveal | mask 100% to 0% | 760ms | cubic-bezier(0.16, 1, 0.3, 1) | none |
| Sticky nav on scroll | -100% to 0 | 240ms | cubic-bezier(0.65, 0, 0.35, 1) | none |
| Modal in | 12px up, scale 0.98 to 1 | 260ms | cubic-bezier(0.34, 1.56, 0.64, 1) | none |
Two rows are doing quiet work. The body copy row says none on purpose, because writing the absence down is the only way a model reliably honours it. And the modal uses the one curve in the table with an overshoot, which is what makes a dialogue feel like it arrived rather than faded up.
Curves, by name and by value#
Every easing word people use in briefs corresponds to a real value, and the values are not interchangeable. These are the ones worth knowing.
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* entrances, decisive */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); /* entrances, softer */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); /* long reveals, 700ms+ */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* things that return */
--ease-back: cubic-bezier(0.34, 1.56, 0.64, 1);/* one accent, never two */
--ease-default: cubic-bezier(0.4, 0, 0.2, 1); /* the framework's, avoid */The last line is the point of the block. cubic-bezier(0.4, 0, 0.2, 1) is Tailwind's stock transition and Material's standard curve, and it is on a significant share of generated pages at its stock 150ms, on every element, including ones that travel 28px. A 28px move in 150ms is a jump. It is not that the curve is bad, it is that it was chosen by nobody, and unchosen values are what make pages converge.
In GSAP the same curves have names: power4.out is close to quint, expo.out to expo, back.out(1.7) to the overshoot. Name either the GSAP string or the bezier, and give the model both if the page mixes CSS transitions with a timeline, so the two halves of the page agree.
Curve and duration are coupled, which is why a brief has to carry both. A quint out at 140ms is indistinguishable from a linear jump, because the slow tail that gives the curve its character needs time to exist. The same curve at 680ms is the reveal in the table above. Pick the duration from the distance travelled: roughly 20ms per 1px of travel for text, halved for anything that only changes opacity, and doubled for a mask crossing a full image.
What must not move#
Half of a motion brief is a refusal list, and it takes four lines. It is also the half that makes two pages built from the same values look unrelated, because what a design declines is far more particular than what it attempts.
- Nothing animates below the second viewport except images, which reveal once and never again on scroll back up.
- No element animates on both entry and exit. One or the other.
- Nothing loops. A looping element in the corner of a page is an ongoing request for attention that the page has not earned.
- Nothing moves more than
32px. Past that it reads as a slide deck.
Then the accessibility half, which is one media query and is missing from most generated output:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}In GSAP the equivalent is gsap.matchMedia() keyed to (prefers-reduced-motion: no-preference), with the reduced branch setting the final state directly. Three lines, and it is the difference between a page that reads a system setting and one that overrides it.
One more refusal belongs in the same list, because it is invisible on a laptop. Hover states do not exist on a touch screen, and a card whose only entrance is a hover lift is a card that never animates for half the audience. Put the hover behind @media (hover: hover) and give touch devices the resting state, at full contrast, with nothing waiting to be revealed by a gesture they cannot make.
The verdict on motion briefs#
Four numbers per element, one curve per page, one refusal list. That is the entire specification, and it fits on a single screen for a whole site rather than per section, which is why there is no excuse for leaving it unwritten.
Order matters in one place only. The hero sequence should finish before the first scroll, which puts a hard ceiling on it: headline at 680ms, subhead starting 120ms later, button at 420ms after that. The whole entrance is done inside 1.3 seconds, and a reader who scrolls immediately never sees a half-finished animation trying to catch up with them.
The test on any motion brief is the same as the test on a three dimensional one: count the values. A brief with no millisecond figure and no bezier in it will return the framework default however long it is, because smooth has never set a duration. A brief with ten numbers in it returns your page, in Claude, Lovable, v0 or Cursor alike.

Writing that table per project is the slow part. Thanor is a subscription library of them: full art direction per design, in exact values, covering motion, three dimensional scenes, animated backgrounds, page sections and app interfaces. $89 for three months, $189 a year, $299 once, with free accounts opening the designs marked free.
Browse the Thanor library and read one design's motion section end to end. If the motion is scroll driven rather than on entry, the distances and scrub values belong in the scroll animation brief, and a free design will show you the format before you decide.
Questions this raises
What makes a good motion design prompt?
Four numbers per element and one named curve for the page. Distance in pixels or percent, duration in milliseconds, an easing curve written as its cubic-bezier value, and a stagger in seconds. Anything less and the framework default fills the gap.
Which easing curve should a website use?
Pick one entrance curve and one exit curve and use them everywhere. cubic-bezier(0.22, 1, 0.36, 1) for entrances reads decisive without overshoot, and cubic-bezier(0.65, 0, 0.35, 1) for anything that moves in both directions.
How long should a website animation last?
Small state changes 120ms to 180ms, entrances 420ms to 680ms, a pinned sequence as long as the scroll distance allows. Past about 800ms an entrance stops reading as motion and starts reading as waiting.
Why do generated pages all animate the same way?
Because the framework default is the same in each of them. Tailwind's stock transition is cubic-bezier(0.4, 0, 0.2, 1) at 150ms, and a brief that names no curve and no duration gets exactly that on every element.
Part of
Build it with AI
Three dimensional scenes, scroll driven motion and animated pages, built by handing a model a specification instead of an adjective. What to write, and what comes back.
