NS Design System
v3.0.0 · 257 tokens

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.

01 · fadeAlready in place, just arrives.
02 · riseThe workhorse — cards, sections, list blocks.
03 · fallOnly for things belonging to what is above them.
04/05 · enter-start / enter-endLogical edges, so they flip in RTL.
06 · expandUncovered rather than moved.
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

fade
rise
fall
enter-start
enter-end
expand
settle — scales DOWN onto the mark; scaling up is the pop P5 forbids
draw — the one animation slow enough to read as a gesture
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.

onviewCompletes before the block is centred, so a fast scroll never shows content assembling.
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