genjutsu

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.