Each block below is one interaction from the motion plan. Everything animates only
transform & opacity (smooth on low-end Chromebooks), and the Reduced motion switch
at the top shows exactly what ships to people who ask for less movement — motion drops to short fades,
nothing disappears. Colored tag = which work-chunk it belongs to.
Part 1 — motion for things you did
You clicked, so something moved. These can be immediate and confident, because
you already know what caused them.
Left-nav slice
Left-nav mode swap + active indicator
Entering program setup slides the whole nav in from the right; leaving slides it back.
The active-item fill slides between rows within a mode — and note the shape shift: pill-shaped items in
the normal nav, squared-off steps in configuration. The one "bigger" moment in the plan, still calm.
Click Enter configuration mode, then click rows to move the indicator.
SSaga
Current program
Reading intervention
Dashboard
Sessions
Roster
Reports
Configure program
Exit configurationReading intervention
Configure
Details
People
Schedule
Groups
Review
The whole panel slides as one unit, so no text ever crosses over other text.
Reduced motion → instant swap, no slide.
Vocabulary slice
Menus & dropdowns — grows from its trigger
Selects, the program picker, and comboboxes scale open from the button rather than
just appearing. ~150ms. Reduced motion → a plain fade.
Reading intervention
Math intervention
All programs
Vocabulary slice
Dialogs — backdrop & panel move as one
The dim backdrop and the dialog share one timing, so they read as a single object arriving.
Reduced motion → both just fade.
Vocabulary slice
Expand / collapse
Period rows and accordions open with a clip + fade instead of snapping. Reduced motion →
instant, no size animation.
Period 2 · Rotation A
Pod 1 — 4 students
Pod 2 — 3 students
Pod 3 — 4 students
Vocabulary slice
Lists — gentle entrance & reorder
Session and pod cards fade up with a light stagger on load, and glide to new positions when
sorted (FLIP) instead of jumping. Reduced motion → they just fade, no travel.
9:00 · Reading — Pod A
9:00 · Math — Pod B
9:40 · Reading — Pod C
9:40 · Math — Pod D
Foundation slice
Loading — skeletons & hold-and-dim
First load shows a shape-matched skeleton (a soft pulse, never a shimmer sweep). When you
re-filter existing data, we hold the old rows and dim them while the new ones fetch — no blank
flash, no layout jump. Reduced motion → skeleton is static.
Ada L. — present
Ben O. — present
Cy R. — absent
Part 2 — motion for things that happened without you
When the data changes on its own
Everything above is motion for something you did. This half is the missing
other one: sessions start, sessions end, filters re-cut the list, notifications arrive — all on a 20-second poll, none of it
asked for.
That difference sets the rules. A change you caused can be instant and confident. A
change you didn't cause has to announce itself without interrupting, and it has to
leave a mark that outlives the animation — because you were probably looking somewhere
else when it happened.
How to read these three. Each has an orange fork box — those are the real design decisions
still open, so flip them and pick. Everything else is a recommendation. Slow-mo makes the timing visible;
Reduced motion shows the accessible fallback.
Ambient tier
Dashboard → Current sessions
Cards fill across, then wrap down (auto-fill, minmax(280px, 1fr)), so a card arriving in the middle
reflows every card after it. Three separate problems: the newcomer, the survivors it displaced, and the gap a departure leaves.
No sessions currently running.
Forks — pick one
Two-phase departure. A leaving card dissolves in place first (180ms); only then do the
survivors close the gap (280ms). Doing it in one phase — yanking everything left the instant a card vanishes — is what makes
live grids feel twitchy. Deliberately not proposed: a different exit for “session ended” vs “filtered out”. You already
know if you changed the filter, and if you didn't you can't tell the two apart anyway — it would double the vocabulary to
convey nothing.
Ambient tier
Sessions → Today
Status section → group container → session rows, each level with its own count and its own collapse. The whole
design turns on one question: is the destination laid out right now? Collapsed bodies
are display:none, so there is nothing to fly into and nothing to measure. That isn't an edge case —
Ended is collapsed by default, which is exactly the blind spot this set out to fix.
Forks — pick one
The hand-off. When the destination isn't laid out, the card can't fly — so it fades and drifts ~22px
toward where that section sits, and the section's header answers: a one-shot tint wash, a chevron nudge, and the count
rolling. Direction alone carries “it went up there” without the target ever being on screen. This is the default, not the
fallback.
Counts are post-filter. The badge shows matches, not the total. So a badge delta has two possible
causes, and if one animation serves both, a single keystroke in the search box rolls every badge on the page at once. Roll on
data, snap on filter.
Ambient tier
Notification bell
The badge only exists when the count is above zero, so 0→1 is an arrival and 1→0 a departure, while 2→3 is
neither. Three different transitions wearing one number.
Reading intervention · Lincoln MS
Forks — pick one
One damped swing, not a shake. −9° → +6° → −3° → 0 over ~460ms, pivoting at the bell's crown. It's a
bell; ringing once is the most obvious thing it can do, and a single decaying swing reads as restraint where a repeat would
read as an alarm. That's the entire whimsy budget for this tier.
Ambient tier
What Part 2 asks of the token layer
Deliberately almost nothing. It reuses Part 1's durations and easings — the one genuinely new value is a decay
window, which is a different kind of thing from a duration, not a fourth duration.
the only new token — how long a “this is new” mark takes to fade
--motion-stagger
32ms · cap 6
existing value, new cap so a bulk change can't become a light show
--ease-out / --ease-over
(.22,1,.36,1) / (.34,1.28,.5,1)
everything / badge pop + bell swing (existing)
Rules the tier adds
Never move what's under the pointer or focus. If the cursor is inside the affected list, or focus is in a card,
hold the reflow until it leaves. A poll must never move a click target out from under a click.
When you defer, coalesce — don't queue and don't drop. A held update that's overtaken by the next poll should be
replaced by it, so one settle lands on the newest truth. Replaying every intermediate state produces motion for changes that
are already stale; dropping the newer one leaves the UI quietly wrong.
Cap the spectacle. Stagger at most 6; past that, land together. If a change replaces more than half a list, don't
animate the members at all — hold-and-dim the container and swap.
Residue over choreography. The mark that lingers does the real work. The motion is only what catches the eye
while you happen to be looking.
Roll on data, snap on filter. A count that moved because you typed is not news.
One live region, aggregated per poll. “2 sessions started, 1 ended” once — not four announcements. Three sections
plus a bell on a 20s interval would otherwise be chatty enough that people switch it off.
Reduced motion keeps the information. Movement goes; residue, tint and digit crossfades stay. Nothing you could
only learn from the animation is lost — swap, don't strip.
Prerequisites found while building this
CountBadge.svelte has no font-variant-numeric: tabular-nums (the bell badge does). Without it
the odometer changes width mid-roll and the header jitters. One line.
Section and container bodies are hidden → display:none. Any FLIP has to check for a laid-out
destination and fall back to the hand-off — that's the branch, not an optimisation.
Related, and a recurring trap: [hidden] is display:none only in the browser's own stylesheet,
so any author display on the same element silently beats it. The repo has already patched this
per-site four times (PeriodContainer, SessionsPage's D35 fix, CohortBand,
PeriodTile) — and I hit it twice more building this page. Every ambient-tier container that collapses needs the
&[hidden]{display:none} guard, so it belongs in the shared layer rather than being rediscovered a fifth
time.
The dashboard grid caps at 24 cards behind “View more”. A new session can arrive and be invisible, and that button is the
only surface that can say so — the same blind spot as collapsed Ended, on a surface nobody had listed.
Don't hand-roll scroll compensation when a new section pushes content down — browser scroll anchoring already does it in
Chrome. Check whether it fights a JS FLIP before designing around either.
Any hover explanation on a residue mark must use Tooltip.svelte; new native title= in
.svelte is CI-blocked.