Files
DenozordecandClaude Sonnet 5.5 f3531c64f9 Move the console onto ReUI PRO
Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move
the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel,
Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame
(DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale.

Design only: behaviour, texts and roles are unchanged. Tests that asserted the old
DOM were updated; the tests now format numbers in English wherever they run.
docs/reui-migration-plan.md records the plan and its decisions.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 00:06:06 +07:00

8.9 KiB

Motion

Installed motion: never retime it

Installed part Duration
Overlays 100ms
Tooltip 150ms (default)
Sheet, sidebar 200ms
Navigation menu 300ms, exit 150ms
rhea control focus 200ms

Never add or change duration-*, ease-*, animate-* or transition-* on installed parts (components/ui, components/reui); Button already presses with active:not-aria-[haspopup]:translate-y-px.

House motion: your own code

Use Tailwind
Hover, press, color transition-colors (150ms, no duration or ease class)
Reveal or expand duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]
Large surface (ceiling) duration-300 with the same curve
An overlay you build duration-100 with the same curve
Media hover zoom duration-500 with the same curve, scale-105 at most
Width tied to the sidebar duration-200 ease-linear
  • Press on a custom surface (not Button): transition-transform active:scale-[0.98] motion-reduce:transition-none.
  • Hover color plus press on 1 element: transition-[color,background-color,border-color,scale] (transition-colors and transition-transform override each other; scale-* sets scale).
  • motion/react takes the same values in seconds: { duration: 0.2, ease: [0.22, 1, 0.36, 1] }; hover { duration: 0.15 }.
  • Drag, reorder and layout use const SPRING = { type: "spring", visualDuration: 0.25, bounce: 0 }. Define SPRING only when something drags, reorders or toggles layout.

Hard rules

  • Animate transform and opacity; color only for hover and press. Never transition-all; width, height, top or left only in the sidebar row.
  • Never animate-bounce or a spring with bounce above 0. ease-linear only for progress, spinners, loops and the sidebar; never Tailwind's ease-in, ease-out or ease-in-out.
  • motion/react only: never framer-motion, GSAP or react-spring.
  • At most 1 entrance animation per view (a stagger counts as 1), and every animation has a reduced-motion path. Feedback that appears after an action or a load (an Alert, a field error, a saved notice, data replacing its Skeleton) is a reveal, not an entrance: fade it in with the reveal row.
  • Never add named duration-* utilities or redefine --ease-out.
  • Loading: a Skeleton (it already pulses) in the real layout; a spinner only inside a busy button.
  • Motion Icons (Ultimate only): 1 on the primary action is the 1 hover flourish; install it with its installAnimated command; never retime it.

Performance

Each line names its lint rule, or judgement (no check).

  • Never animate blur, drop-shadow, backdrop-filter, box-shadow, border-radius, background-position, background-size or a custom property that feeds transform; clip-path and stroke-dashoffset only as a one-shot reveal of 300ms or less, or a loop on an element of 240x240px or less. Name the properties: never a bare transition (it also animates box-shadow and filter). (reui/no-paint-animation; 240px: judgement (no check))
  • A hover lift is transform over a static shadow, or a shadow layer whose opacity fades; never a transitioned hover:shadow-*. (reui/no-paint-animation)
  • CSS first: hover, press, reveals and ambient loops are Tailwind transitions, tw-animate-css or @keyframes. motion/react only for exit on unmount, drag, springs that follow input, layout and scroll-linked values. (judgement (no check))
  • In motion/react, x, y, scale, rotate, pathLength and every SVG target run on the main thread: never repeat: Infinity on them (loop with CSS @keyframes), and keep 20 or fewer JS-driven animations running per view. An opacity loop leaves the main thread only on an HTML element without repeatDelay, repeatType: "mirror" or onUpdate. (reui/no-main-thread-loop)
  • SVG geometry (a rect's width, a circle's r) does not reflow the page but runs in motion's JS loop: one-shot only. (reui/no-main-thread-loop)
  • Never set React state per frame (requestAnimationFrame, useAnimationFrame, onUpdate, scroll or pointermove listeners): bind styles to useMotionValue, useTransform or a CSS variable on the element. (reui/no-frame-setstate)
  • A JS-driven loop (motion repeat, requestAnimationFrame, canvas) pauses offscreen with useInView or IntersectionObserver; content-visibility: auto never stops script, and a setInterval loop also pauses on visibilitychange. (reui/loop-offscreen)
  • No permanent will-change: no will-change-* class or willChange style, except during a drag, removed when it ends. (reui/will-change)
  • Stagger 50ms or less per step (staggerChildren: 0.05), only the first 6 items, the whole entrance done in 500ms, first mount only, never on table rows or lists of 20 or more; scroll reveals use whileInView with viewport={{ once: true }}. (reui/stagger-budget; item count and tail: judgement (no check))
  • A height reveal (grid-template-rows) starts only on a click or key press, runs 200ms or less, 1 at a time; never initial={{ height: 0 }}. (reui/no-layout-transition)
  • The motion layout prop on 1 list at a time (about 20 elements), never in data-grid or virtualized rows; drag and drop goes through @reui/sortable or @reui/kanban. (judgement (no check))
  • AnimatePresence mode="wait" only with an exit of 150ms or less; initial={false} around content present at first render. (reui/stagger-budget)
  • Never hide the hero or the largest heading behind a motion/react initial at opacity 0 (hidden until JavaScript runs). (judgement (no check))
  • At most 3 ambient loops per view: CSS @keyframes on transform or opacity, a period of 2s or more, exempt from the duration table; once 3 run in a view they pause offscreen (a content-visibility: auto wrapper with contain-intrinsic-size: auto <h> where nothing paints outside it, or IntersectionObserver toggling animation-play-state). At most 50 animating elements per view (Skeletons, loaders and CSS opacity loops not counted). A static blur above 12px, at most 1 per view, only on a surface that neither moves nor sits over motion. (judgement (no check))

Reduced motion

  • Reduced motion is a substitute, not an off switch: replace movement (translate, scale, rotate, slide, zoom, layout, parallax, auto-scroll) with an opacity fade of 150ms or less or an instant change. Keep color and opacity feedback, focus, progress bars, loaders, the Skeleton pulse, chevrons and the switch thumb. (reui/reduced-motion)
  • The reduced state is the resting state: content never waits at opacity 0, hidden or offset for an animation that will not play; a count-up shows its final value. (reui/reduced-motion)
  • In every mode an entrance moves 8px or less and starts at scale 0.95 or more; nothing moves a surface larger than half the viewport. (reui/reduced-motion; half the viewport: judgement (no check))
  • <MotionConfig reducedMotion="user"> once at the app root (its default ignores the OS) AND a motion-reduce: path on every CSS animation (MotionConfig never reaches CSS). An example or block wraps its own motion subtree. (reui/reduced-motion)
  • MotionConfig makes only transform keys and width, height, top and left instant; opacity, filter, clipPath, pathLength, color and a transform string keep animating, so a loop or those keys branch on useReducedMotion() and rest on the final value. (reui/reduced-motion)
  • useReducedMotion() is null on the server: never let it pick markup (JSX, className, initial) or reach animate under initial={false}; branch only animate, transition, while* or exit values. (reui/reduced-motion-ssr)
  • No SMIL (<animate>, <animateMotion>, <animateTransform>); canvas and requestAnimationFrame loops draw 1 resting frame under prefers-reduced-motion: reduce. (reui/no-smil, reui/reduced-motion)
  • Smooth scroll only under motion-safe:scroll-smooth. A focus ring appears at once: never transition-all, transition-shadow or a bare transition on it. (reui/reduced-motion, reui/focus-transition)
  • Loaders and typing dots keep their opacity pulse; every decorative loop gets motion-reduce:animate-none, never an inline style={{ animation }} (it beats the class). (reui/reduced-motion)
  • Anything that moves on its own for more than 5 seconds (autoplay, marquee, ticker) gets a visible Pause control, pauses on hover and focus, and never autoplays under reduced motion. (reui/pausable-autoplay)
  • Installed overlays keep their 100ms fade, zoom and 8px slide under reduced motion: disclosed, not edited; an app that needs WCAG 2.3.3 adds its own override. (judgement (no check))