Skip to content

Motion

Motion brings interfaces to life — but only when used with purpose. Tessera UI’s motion system is minimal and functional, providing feedback for interactions without introducing distraction or delay.

  1. Motion communicates, it doesn’t decorate. Every animation should help the user understand what happened — an element appeared, moved, or changed state.
  2. Fast by default. Most interactions use 160ms or less. Users should never wait for an animation to finish.
  3. Consistent easing. A single easing curve creates a unified feel across all transitions.
  4. Respect user preferences. Animations should be reduced or removed when prefers-reduced-motion is active.
TokenValueUsage
--ts-transition-fast160msMicro-interactions: button hover, toggle switch, focus ring, color changes
--ts-transition-normal240msState transitions: card hover elevation, component enter/exit
--ts-transition-slow360msLayout shifts: accordion expand, panel slide, page transitions
--ts-transition-micro80msInstant-feeling micro-feedback: checkbox tick, ripple start
--ts-transition-x-slow500msLarge-scale transitions: page-level entrances, complex orchestrations
TokenValueUsage
--ts-loading-pulse-duration1.5sSkeleton pulse cycle
--ts-loading-shimmer-duration1.8sShimmer/sweep animation
  • fast (160ms) — Use for anything that responds to direct user input: hover effects, active states, focus indicators, toggle slides. The user should feel an instant response.
  • normal (240ms) — Use for visual transitions that involve spatial change: a card lifting on hover, a modal scaling in, a tooltip appearing. Slightly longer to let the eye track the motion.
  • slow (360ms) — Use sparingly for larger layout changes: accordion sections expanding, panels sliding in from the edge. Reserved for motions that affect page layout.

Tessera UI provides four easing curves as tokens, each suited to a different type of motion:

TokenValueUsage
--ts-ease-standardcubic-bezier(0.4, 0, 0.2, 1)Default for all transitions — natural deceleration
--ts-ease-deceleratecubic-bezier(0, 0, 0.2, 1)Elements entering the screen — starts fast, slows into place
--ts-ease-acceleratecubic-bezier(0.4, 0, 1, 1)Elements leaving the screen — starts slow, accelerates out
--ts-ease-springcubic-bezier(0.16, 1, 0.3, 1)Playful, bouncy motion — modal entrance, toast pop-in
  • Standard — The workhorse. Use for hover effects, color transitions, and most state changes.
  • Decelerate — Use when an element enters the viewport (slide-in panels, toasts appearing). The fast start draws the user’s eye.
  • Accelerate — Use when an element exits. The slow start gives the user a moment to register the departure.
  • Spring — Use sparingly for emphasis. Modal entrances and toast notifications benefit from the slight overshoot.
/* Active state */
transform: scale(0.97);
transition: transform var(--ts-transition-fast);

A subtle scale-down on press provides tactile feedback. The 3% reduction is enough to feel without being distracting.

/* Overlay */
@keyframes ts-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Duration: 200ms, ease-out */
/* Dialog */
@keyframes ts-scale-in {
from { opacity: 0; transform: scale(0.95) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
/* Duration: 240ms, cubic-bezier(0.16, 1, 0.3, 1) */

Modals use a two-part entrance: the overlay fades in first, then the dialog scales up from slightly below center. The dialog uses a more pronounced ease-out (spring-like) for a polished feel.

opacity: 0 → 1;
transform: scale(0.95) → scale(1);
transition: opacity, transform var(--ts-transition-fast);

Tooltips fade and scale up from 95% — a quick, minimal entrance that doesn’t draw focus.

transition: left var(--ts-transition-fast);
/* Track background also transitions */
transition: background-color var(--ts-transition-fast);

The thumb slides horizontally while the track color transitions simultaneously.

transition: box-shadow var(--ts-transition-normal), transform var(--ts-transition-normal);
/* On hover: shadow increases, card lifts 2px */

Interactive cards use normal duration because the shadow spread change is spatially larger than a color change.

Always list specific properties in transitions — never use transition: all:

/* Good */
transition:
background-color var(--ts-transition-fast),
border-color var(--ts-transition-fast),
color var(--ts-transition-fast);
/* Bad — triggers layout recalculation for every property */
transition: all var(--ts-transition-fast);

When multiple elements enter the screen at once (list items, grid cards, dashboard widgets), stagger their entrance to create a choreographed sequence rather than a simultaneous flash.

Tessera UI provides a stagger interval token:

TokenValueUsage
--ts-stagger-interval50msDelay between each item’s entrance animation

Implementation example:

.stagger-list > * {
opacity: 0;
transform: translateY(8px);
animation: ts-stagger-enter var(--ts-transition-normal) var(--ts-ease-decelerate) forwards;
}
/* Each child delays by its index * interval */
.stagger-list > *:nth-child(1) { animation-delay: calc(1 * var(--ts-stagger-interval)); }
.stagger-list > *:nth-child(2) { animation-delay: calc(2 * var(--ts-stagger-interval)); }
.stagger-list > *:nth-child(3) { animation-delay: calc(3 * var(--ts-stagger-interval)); }
.stagger-list > *:nth-child(4) { animation-delay: calc(4 * var(--ts-stagger-interval)); }
/* ... continue as needed, or use a CSS custom property with JavaScript */
@keyframes ts-stagger-enter {
to {
opacity: 1;
transform: translateY(0);
}
}

Guidelines for stagger:

  • Cap stagger at 8-10 items — beyond that, the total delay becomes noticeable
  • Total stagger sequence should not exceed 500ms (--ts-transition-x-slow)
  • Use prefers-reduced-motion to disable stagger entirely, showing all items immediately

Users who set prefers-reduced-motion: reduce in their OS or browser preferences should see minimal or no animation. Components should respect this:

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

This is recommended at the application level. Essential state changes (like toggle position) should still be instant but skip the animation.

  1. Never block interaction with animation. Users should be able to click, type, or navigate without waiting for an animation to complete.
  2. Don’t animate layout properties (width, height, top, left). Use transform and opacity for performant animations that don’t trigger layout recalculation.
  3. Match duration to distance. Small movements (hover color) = fast. Medium movements (card lift) = normal. Large movements (panel slide) = slow.
  4. Entrance animations only. Components animate when appearing but disappear instantly (or with a very fast fade). Users don’t want to wait for something to leave.