Skip to main content

Motion

Implements PLAN §4.1. Motion explains a change; it never delays one.

Hard rules (from PLAN §4.1 and CLAUDE.md)

  1. Only the motion tokens: --dur-instant, --dur-fast, --dur-base, --dur-slow, --dur-flash, --ease-out, --ease-in. No literal durations or easings in components.
  2. Only opacity and transform are transitioned or animated. Do not use transition-all or transition-colors (shadcn ships these; FE-Platform replaces them with transition-[opacity,transform]). A colour that should fade is drawn on a layer whose opacity changes.
  3. Prices, sizes, P&L, balances, scores and counts are never tweened or counted up. They change on the frame the data arrives.
  4. Ladders and books never scroll or re-centre on their own while the pointer or a finger is over them, and never with animation.
  5. No layout animation (height, width, top, grid rows) on anything holding streaming data. Layout changes happen in one frame; the content inside may then fade.
  6. Actions fire on input, not on animation end. An exiting element is already inert (pointer-events: none, inert) on its first exit frame.
  7. Enter uses --ease-out. Exit uses --ease-in at 0.75× the enter duration: calc(var(--dur-base) * 0.75).
  8. Nothing is longer than --dur-slow (240 ms) except --dur-flash (400 ms, set by the PLAN table for the price flash and reused for the two other one-shot highlights: score change and Safe nudge) and the delay countdown ring (a timer that runs for the real delay, not a transition).
  9. No looping decoration: no shimmer, no pulsing dots, no spinning except a 14 px loading spinner while a request is in flight.

Reduced motion

tokens.css sets --dur-fast, --dur-base, --dur-slow and --dur-flash to 0ms under prefers-reduced-motion: reduce, so token-based CSS turns instant by itself. Libraries and APIs that do not read the tokens need their own switch:

Per component

“Instant” = --dur-instant (no transition).

Price flash

A short background flash when a displayed price changes, green for up and red for down. Opacity only; layout never moves.
  • Which values flash: best bid/ask prices on price buttons and price cells, last price, and P&L cells. Sizes, depth and counts do not flash.
  • Where: a ::after layer (inset: 0, pointer-events: none, --radius of its cell), under the text, with background --flash-up or --flash-down.
  • Animation: on a change, the layer jumps to opacity 1 and fades to 0 over --dur-flash with --ease-in. A new change during a flash restarts it (use the Web Animations API el.animate() or re-key the layer; do not queue flashes).
  • Tinted buttons: the tint layer (--bid-muted / --ask-muted) drops to opacity 0 at the same moment and returns to 1 over --dur-flash, so flash and tint never stack (contrast is checked for that case in tokens.md).
  • When not to flash: on first render, on a snapshot or resync, on the first value after stale recovers, while stale, and while the element is hovered as a trade target (the solid hover fill already covers it).
  • Reduced motion: no flash. A static lucide [arrow-up] in --bid or [arrow-down] in --ask appears after the value, positioned absolutely outside the value’s text box so the value stays centred and nothing shifts, and clears on the next change or after 5 s.
--dur-flash (400 ms) is the one duration above --dur-slow. PLAN §4.1 sets 400 ms for the flash explicitly, and CLAUDE.md caps durations at --dur-slow; open question 1 asks to record this exception in CLAUDE.md. Covered by FE-Platform; listed here so reviews check it with the motion:
  • Routes preload on hover or focus (preload: "intent").
  • placeholderData: keepPreviousData on route queries: the old screen stays until the new data is ready; skeletons only on a cold load.
  • Scroll position is restored per route; board, watchlist and portfolio keep their state across tab switches (mobile tabs stay mounted).
  • Route chunks are prefetched when idle.