Motion

How BDS components animate — timing, easing, transitions, and icon morphs.

Motion in BDS is short, purposeful, and never in the way. It confirms that something happened, shows where an element came from, and then gets out of the way. These rules apply to every component in @base-org/bds-web and to product code built on it. Like the rest of the Guidelines, they are defaults: follow them unless the context calls for an exception, and note why when you deviate.

Durations today

Components currently animate at 135ms, 150ms, and 200ms, with a few one-offs: 75ms exits on Tooltip and Popover, a 105ms exit on Dialog, and 140ms on Tabs. The Metric Card line chart is a deliberate exception: it draws in over 600ms the first time it scrolls into view. Loading loops (Skeleton and Shimmer Overlay at 1.6s, the chart placeholders at 2.5s) repeat until data arrives. Named motion tokens — so durations and easings are referenced by purpose rather than typed as numbers — are planned. Until they land, match the closest existing duration instead of inventing a new one.

Timing

  • Keep animations under 300ms unless there is a specific reason not to.
  • Keep frequent interactions, such as a hover colour change, instant or very fast.
  • Disable all animations and transitions for keyboard interactions — for example data-focus-visible:transition-none. Someone tabbing through a page should never wait for motion to finish.
  • Don't animate on page load unless it is intentional.
  • Animate groups of entering elements in small groups with a short stagger delay, not as one giant block.

Easing

Pick the easing from what the element is doing:

Is the element entering or exiting the viewport?
├── Yes → ease-out
└── No
    ├── Is it moving/morphing on screen?
    │   └── Yes → ease-in-out
    └── Is it a hover change?
        ├── Yes → ease
        └── Is it constant motion?
            ├── Yes → linear
            └── Default → ease-out

Always write the easing out, even when it is ease. The browser default is ease, but many people assume it is linear, so an omitted easing is ambiguous to the next reader.

Do

transition: opacity 150ms ease; — the easing is stated, even though it matches the default.

Don't

transition: opacity 150ms; — the reader has to guess whether linear or ease was meant.

Writing transitions

  • Name the exact properties you animate; never use transition: all.
  • When several properties share a duration and easing, set the timing once and list the properties separately, so changing the timing is a one-line edit.
  • Write delays as their own transition-delay, not inside the shorthand, where they are easy to misread as the duration.

Do

transition-property: opacity, scale;
transition-duration: 200ms;
transition-timing-function: ease-out;
transition-delay: 50ms;

Don't

transition:
  opacity 200ms ease-out 50ms,
  scale 200ms ease-out 50ms;

In Tailwind, the same split reads as transition-[opacity,scale] duration-200 ease-out delay-50.

Transitions vs. keyframes

Use CSS transitions whenever an animation can be interrupted or retargeted: an interaction that can change direction mid-animation, or an entrance whose end position can change while it runs, such as a stack of toasts that shifts when a new one arrives. A transition picks up from wherever the element is; a keyframe animation restarts or jumps. Save keyframes for one-off sequences that always play the same way.

Hover and focus

  • When an element moves on hover, put the hover on a still parent and move only the child. If the hovered element itself moves, it can slide out from under the cursor, lose hover, snap back, and flicker.
  • Anything revealed on hover must also be revealed on keyboard focus, with :focus-visible or :focus-within, so keyboard users can reach it.
<a href="#" className="group inline-block rounded-lg p-3">
<span className="block transition-transform duration-200 ease-out group-hover:-translate-y-1 group-focus-visible:-translate-y-1">
  Lift on hover
</span>
</a>

The link stays still and owns the hover; only the inner label moves, and it moves on focus too.

Entrances and exits

  • Animate elements from the trigger's position, not the centre — set transform-origin accordingly.
  • Skip the opening animation for menus that are opened often; only animate on close.
  • Make exit animations more subtle than entrances: shorter travel distance, fading with opacity and a 4px blur.

Feedback

  • Make buttons scale down when pressed (scale 0.95–0.98, transition: scale 200ms ease-out).
  • Cross-fade icon swaps: the new icon scales 0.25 → 1, opacity 0 → 1, blur 4px → 0px, and the old icon does the reverse.

Implementation

  • Import from motion/react, never framer-motion.
  • Don't animate the switch between light and dark mode. Every element with a colour transition would fade at its own duration, so the page smears through half-themed states instead of changing at once. ThemeProvider turns transitions off for the frame it swaps the dark class; if you switch themes any other way, do the same.
  • Add will-change: transform to elements that jitter 1–2px during animation, especially in Safari on iOS.

Reduced motion

Put animations inside @media (prefers-reduced-motion: no-preference), so they only run for people who haven't asked for less motion. When reduced motion is on, state changes — including icon morphs — happen instantly.

Icon motion

  • When a simple line icon changes state (menu ↔ close, play ↔ pause, plus ↔ minus, chevron direction), transform the shape — don't cross-fade two SVGs.
  • Morphable icons share one structure: exactly three lines. Icons that need fewer collapse the extras to an invisible point at the centre, so any morphable icon can become any other.
  • Icons that are the same shape at a different angle (arrows, chevrons, plus/cross) form a rotation group: rotate between them, never interpolate their coordinates. Coordinate morphing on these makes lines bend and warp.
  • Icons from different shapes interpolate line coordinates (arrow → check, menu → play).
  • Detailed or filled icons from the Figma icon set don't morph; use the icon cross-fade under Feedback.
  • A morph is movement on screen, so it follows the timing and easing rules above: under 300ms, ease-in-out.
  • Match the morphing icon's stroke caps and width to the static icon set.
  • Judge morphs by watching them. Every new icon or transition pair is reviewed by a person in a playground that cycles through transitions. Agents optimise for "works", not "feels right", so they can't sign off on motion alone.

Credits

The transition rules under Easing, Writing transitions, Transitions vs. keyframes, and Hover and focus draw on ideas from Animations on the Web by Emil Kowalski, written here in our own words.