45Page motion
Nine entrance animations, and a hard limit on what they are for. Principle 5 governs interaction — 120–180ms, no bounce. Entrance is the one place a longer curve is allowed, because nobody is waiting on it: its job is to tell the eye what order to read a page in.
Use it for
- One entrance per BLOCK — a section, a card grid, a band
- --onview for content below the fold
- --stagger on a container whose children arrive together
Not for
- One animation per element — a page where twelve things fly in individually is a page nobody can read while it assembles
- Anything travelling further than half a rem; past that it reads as a slide deck
- Interaction feedback — a hover or a press is 120ms and lives on the component
The nine
Reload the page to replay. rise is the workhorse; fall is only for things that belong to what is above them.
markup
<div class="ns-anim-stagger" style="display:grid;gap:var(--space-2)">
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">01 · fade</span><span class="ns-card__text">Already in place, just arrives.</span></div></div>
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">02 · rise</span><span class="ns-card__text">The workhorse — cards, sections, list blocks.</span></div></div>
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">03 · fall</span><span class="ns-card__text">Only for things belonging to what is above them.</span></div></div>
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">04/05 · enter-start / enter-end</span><span class="ns-card__text">Logical edges, so they flip in RTL.</span></div></div>
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">06 · expand</span><span class="ns-card__text">Uncovered rather than moved.</span></div></div>
</div>Individually
markup
<div class="ns-anim ns-card"><div class="ns-card__body"><span class="ns-card__kicker">fade</span></div></div>
<div class="ns-anim ns-anim--rise ns-card"><div class="ns-card__body"><span class="ns-card__kicker">rise</span></div></div>
<div class="ns-anim ns-anim--fall ns-card"><div class="ns-card__body"><span class="ns-card__kicker">fall</span></div></div>
<div class="ns-anim ns-anim--enter-start ns-card"><div class="ns-card__body"><span class="ns-card__kicker">enter-start</span></div></div>
<div class="ns-anim ns-anim--enter-end ns-card"><div class="ns-card__body"><span class="ns-card__kicker">enter-end</span></div></div>
<div class="ns-anim ns-anim--expand ns-card"><div class="ns-card__body"><span class="ns-card__kicker">expand</span></div></div>
<div class="ns-anim ns-anim--settle ns-card"><div class="ns-card__body"><span class="ns-card__kicker">settle — scales DOWN onto the mark; scaling up is the pop P5 forbids</span></div></div>
<div><span class="ns-anim ns-anim--draw" style="display:block;block-size:2px;background:var(--color-brand-500)"></span><span class="ns-card__kicker">draw — the one animation slow enough to read as a gesture</span></div>Stagger, capped
40ms apart, capped at the fifth child. Past ~200ms of accumulated delay the last item feels broken rather than choreographed — every stagger that hurts is one that multiplied the index without a ceiling.
- 01Arrives first
- 02+40ms
- 03+80ms
- 04+120ms
- 05+160ms
- 06+200ms — and everything after
- 07+200ms
markup
<ul class="ns-anim-stagger ns-list">
<li class="ns-list__row"><span class="ns-list__index">01</span><span class="ns-list__title">Arrives first</span></li>
<li class="ns-list__row"><span class="ns-list__index">02</span><span class="ns-list__title">+40ms</span></li>
<li class="ns-list__row"><span class="ns-list__index">03</span><span class="ns-list__title">+80ms</span></li>
<li class="ns-list__row"><span class="ns-list__index">04</span><span class="ns-list__title">+120ms</span></li>
<li class="ns-list__row"><span class="ns-list__index">05</span><span class="ns-list__title">+160ms</span></li>
<li class="ns-list__row"><span class="ns-list__index">06</span><span class="ns-list__title">+200ms — and everything after</span></li>
<li class="ns-list__row"><span class="ns-list__index">07</span><span class="ns-list__title">+200ms</span></li>
</ul>Scroll-triggered
--onview uses native animation-timeline: view() — no observer, no JS. Where unsupported the declaration is dropped and it runs on load, which is visible either way.
markup
<div class="ns-anim ns-anim--rise ns-anim--onview ns-card"><div class="ns-card__body"><span class="ns-card__kicker">onview</span><span class="ns-card__text">Completes before the block is centred, so a fast scroll never shows content assembling.</span></div></div>Accessibility contract
- All nine are off under prefers-reduced-motion: the global guard in tokens/effects.css collapses every animation to 0.001ms, so none of these carries its own media query
- That is deliberate — a per-component opt-out is a per-component chance to forget
- Nothing here gates content: every element is readable if the animation never runs