Skip to main content
ENKO

Motion

Motion shows users what changed and where things went. Every animation in Senco must be purposeful — never decorative.

There is a second reason motion is tightly constrained here. Senco's audience runs mid-range Android hardware on unreliable connections, and animation is the first thing that degrades: a 300ms transition that drops to 8fps reads as the app freezing at the exact moment a user is confirming a payment. Motion is a budget, and this page is where it is spent.

Duration tokens

Duration tokens define how long an animation lasts. Shorter durations suit small, frequent interactions; longer durations suit larger, less frequent transitions.

TokenValueUsage
--ds-duration-instant50msButton press, toggle, checkbox
--ds-duration-fast100msDropdown open, tooltip appear, hover state
--ds-duration-normal180msPanel slide, toast enter/exit, accordion
--ds-duration-slow300msPage transition, modal open/close
--ds-duration-slower500msSkeleton fade, complex transitions

--ds-transition composes the default pair — 180ms cubic-bezier(0.4, 0, 0.2, 1) — and is what a component should reach for when it has no reason to choose otherwise.

Choosing a duration

  • Smaller elements animate faster. A checkbox toggle should feel instantaneous.
  • Larger surfaces use longer durations so the eye can track the change.
  • Frequent actions (e.g., hover) should use fast durations to avoid feeling sluggish.
  • Infrequent actions (e.g., page transitions) can afford slower, more expressive timing.

Rule of thumb: the duration scales with the distance travelled, not with the importance of the element. An important button that moves 4px still animates at 50ms.

The 300ms ceiling

Nothing in Senco animates longer than 300ms except skeleton fades. Above roughly 400ms a transition stops reading as feedback and starts reading as latency, and in a transfer app the user cannot tell the difference between a slow animation and a slow network. When in doubt the interface should feel abrupt rather than uncertain.

--ds-duration-slower (500ms) exists only for skeleton crossfades, where the slowness is the point — a skeleton that snaps to content flashes.

Do / Don't
Do

Subtle 150ms ease on hover. Functional feedback without distraction.

Don't

Flashy 2s bouncing animation. Distracting, inaccessible, and unprofessional.

Duration tokens
duration-instant
70msButton press, toggle, checkbox
duration-fast
150msDropdown, tooltip, hover state
duration-moderate
240msPanel slide, toast, accordion
duration-slow
400msPage transition, modal, skeleton

Easing curves

Easing curves control the acceleration profile of an animation. Senco uses four curves.

TokenValueUsage
--ds-ease-defaultcubic-bezier(0.4, 0, 0.2, 1)Most transitions — elements moving between states
--ds-ease-outcubic-bezier(0, 0, 0.2, 1)Elements appearing (entrance) — decelerate into place
--ds-ease-incubic-bezier(0.4, 0, 1, 1)Elements leaving (exit) — accelerate out
--ds-ease-bouncecubic-bezier(0.34, 1.56, 0.64, 1)Playful feedback (success checkmark, toggle snap)

When to use each curve

  • Standard is the default. Use it for any state change that doesn't involve an element entering or leaving the DOM.
  • Entrance for modals opening, toasts appearing, dropdowns expanding.
  • Exit for modals closing, toasts dismissing, dropdowns collapsing.

Entrance decelerates because the element is arriving and the eye needs to catch it. Exit accelerates because the element is already understood and lingering on it wastes the user's attention.

Where bounce is allowed

--ds-ease-bounce overshoots, and overshoot in a money app reads as unseriousness. It is permitted in exactly two places:

AllowedNot allowed
Success checkmark on a completed transferAny error, warning, or failed state
Toggle and switch snapModal, sheet, or page transitions

A transfer completing is the one moment in the product where a small celebration is earned. Everywhere else, bounce makes a financial interface feel like it is playing.

Easing curves
Standard
Toggle switch
cubic-bezier(0.2, 0, 0.38, 0.9)
Entrance
Dot wave loading
cubic-bezier(0, 0, 0.38, 0.9)
Exit
Card fade-out
cubic-bezier(0.2, 0, 1, 0.9)

Reduced motion

Users who enable prefers-reduced-motion on their OS must never see animations that could cause discomfort. Senco respects this setting globally.

The implementation is a single Platform-layer block, not a per-component guard:

@media (prefers-reduced-motion: reduce) {
  .ds-layout *,
  .ds-layout *::before,
  .ds-layout *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Handling it once means a component written next year is compliant without its author knowing this rule exists. Durations collapse to 0.01ms rather than 0 so that transitionend and animationend listeners still fire — setting 0 silently breaks any component whose logic waits for an animation to finish.

What to simplify

CategoryDefaultReduced motion
Transitions (opacity, color)Animate normallyInstant
Slide/transform animationsAnimate with easingReplace with opacity-only fade
Loading spinnersContinuous rotation, 1s linearRotation stops; the glyph stays visible and the label carries the state
Auto-playing carouselsAnimate between slidesPause, show manual controls
Status timeline pulsePulsing active dotStatic dot, state carried by color + icon + label

The platform block above does not exempt spinners: animation-iteration-count: 1 at 0.01ms stops every rotation. That is deliberate — one mechanism, no per-component escape hatches. What keeps a pending state informative is that every spinner in the system is paired with a text label (see Inline loading), so a static glyph plus "Checking recipient details" says exactly what a rotating one did. Reduced motion means less motion, not less information.

The global block is a floor, not a ceiling

The Platform rule neutralizes transitions, but a component that communicates only through motion is still broken for these users. Any animated state must have a non-animated counterpart — which is the same triple-signaling rule that governs status color.

Reduced motion
Full Motion
Confirm transfer?$250.00 CAD to Adaeze O.
Cancel
Confirm
Reduced Motion
Confirm transfer?$250.00 CAD to Adaeze O.
Cancel
Confirm
Left: Full animation with slide, scale, and opacity transitions. Right: Only opacity changes — no spatial movement. This is what users with prefers-reduced-motion: reduce will see.

Design and accessibility

  • Purpose over polish. Every animation must answer: "What does this help the user understand?"
  • No animation-only feedback. If motion communicates a state change, pair it with a visual change (color, icon, text) that persists after the animation ends.
  • Respect user preference. Always honor prefers-reduced-motion. Never override it.
  • Performance budget. Animate transform and opacity for anything that moves, and background-color, border-color, color, and box-shadow for state feedback. Never animate width, height, top, left, margin, or padding — those trigger layout on every frame. Colour and shadow do not move an element, so they do not trigger layout; they are what every documented button, card, and input state change actually uses.
  • Test on low-end devices. Animations that are smooth on a MacBook Pro may stutter on a budget Android phone — Senco's primary audience.
  • No motion above 3 flashes per second (WCAG 2.3.1). Nothing in the system currently approaches this, and nothing should be added that does.
  • Motion never gates input. A user must be able to tap the primary action while an entrance animation is still running.

Do and don't

DoDon't
Animate transform, opacity, colour, and shadowAnimate height to expand an accordion
Use bounce on a completed transferUse bounce on a failed one
Pair every spinner with a text label, so a static one still communicatesShip a bare spinner that becomes meaningless once it stops
Keep transitions under 300msAdd a 600ms "premium" page transition
Pair a state animation with a persistent visual changeRely on a flash to communicate that a value updated

Known gaps

ItemStatus
Motion token coverageDurations and easings are tokenized; no tokens exist for delay or stagger
Low-end device testingStated as a requirement; no documented device matrix or frame-rate budget
forced-colors interactionUntested alongside reduced motion