Principles
The rules genjutsu enforces, and why each one exists: duration by context, easing by direction, reduced motion as a requirement, and the five prohibitions.
All documentation
These are the positions the whole thing is built on. They are not preferences and they are not configurable, because a position you can switch off is not a position.
Duration comes from context, not taste
| Context | Duration | Why |
|---|---|---|
| Micro-interaction: toggle, hover, focus | 100 to 150ms | Instant feedback, no perceived delay |
| UI transition: modal, drawer, tab switch | 200 to 300ms | Smooth without dragging |
| Page or route transition | 300 to 500ms | Long enough to establish where you went |
| Scroll-driven or 3D | No duration | Tied to input, so it is progress-based |
And the rule underneath the table: the more often an animation plays, the shorter and subtler it has to be. A button hover fires a thousand times a day and gets 100ms of opacity. An onboarding reveal fires once, ever, and can afford 600ms of full choreography. Applying the onboarding budget to the button is how an interface becomes exhausting.
Easing comes from direction
| Action | Easing | Why |
|---|---|---|
| Entering | ease-out, or a spring |
Decelerates into place, like arriving |
| Exiting | ease-in |
Accelerates away, getting out of the way |
| Moving between states | ease-in-out |
Smooth at both ends |
| Scroll-synced | linear |
Anything else reads as lag against the input |
| Playful | Underdamped spring | Overshoot is what reads as alive |
Exit is always more subtle than enter. Enter can be 300ms with translate, opacity and scale together. Exit should be 200ms of opacity. Asymmetry in that direction is correct; the reverse is a bug that feels like the interface is reluctant to let go.
Reduced motion is a requirement
Not a nice-to-have, not a phase-two item, and not something to add if a user complains.
| Platform | API |
|---|---|
| Web, CSS | @media (prefers-reduced-motion: reduce) |
| Web, JS | matchMedia('(prefers-reduced-motion: reduce)') |
| SwiftUI | @Environment(\.accessibilityReduceMotion) |
| UIKit | UIAccessibility.isReduceMotionEnabled |
| Compose | A helper over Settings.Global.ANIMATOR_DURATION_SCALE |
An animated project with no handler anywhere is a critical audit failure. And respecting the preference means providing the finished state, not a broken one: a reduced-motion visitor should see the composition, just not the journey to it.
Two more, easy to forget: focus indicators must never be hidden by an animation, and animated text must meet its contrast ratio at every frame, not just at the end. Mid-transition is where fading text quietly drops below the line.
The five prohibitions
Each of these has a wrong and a right form in all three stacks.
1. Never animate width, height, top or left. They trigger layout on
every frame. Animate transform and opacity, which composite on the GPU. In
Compose that means graphicsLayer { translationY } rather than
animateDpAsState on a size. In SwiftUI it means .offset rather than
.frame(height:).
2. Never scale to zero. scale(0) makes an element vanish into a point.
Scale to 0.95 and fade instead.
3. Never ease-in on an entrance. Ease-in means a slow start, and an element that hesitates on the way in reads as broken.
4. Never exceed 500ms on a UI interaction. Modals, dropdowns, tooltips and tabs all have a user waiting on the other side of them.
5. Never skip reduced motion. See above.
Performance is part of the design
Only transform and opacity get animated, because they are the composite-only
properties. will-change is used sparingly and removed when the animation ends,
because permanently promoting a dozen elements costs more GPU memory than it
saves in paint. Animation loops use requestAnimationFrame, never setTimeout,
which drops frames and keeps running in a background tab.
For scroll-driven work the order of preference is CSS animation-timeline first,
then IntersectionObserver, then a scroll listener as a last resort. And the
target gets verified on throttled hardware, not on the machine that wrote it.
Consistency is auditable
A well-designed project uses three to five distinct durations and three to five
named easings. Not because variety is bad, but because twelve different
cubic-bezier values scattered across a codebase are not a design decision, they
are the absence of one. When the audit finds that many, the fix is to name them
and centralise them, not to argue for them.