Product motion
A reference for how motion is applied across the GoCardless product — covering easing curves, duration tiers, and practical guidance for building consistent, purposeful animations.
Motion Principles
We follow three principles for applying motion in the GoCardless product.
Every animation should earn its place by serving a function: confirming an action, guiding attention, communicating state, or smoothing a transition.
Animation should feel fast. Users are here to get things done — nothing should linger. When in doubt, use a shorter duration.
Motion should enhance the experience without drawing attention to itself. The product content is the hero, not the animation.
Primary Easing Curves
Our three core curves with their intended use-cases.
cubic-bezier(0.5, 0, 0, 1)
- Dialogs, modals & drawers
- Step and page transitions
- Progress bars & accordions
- Default when uncertain
cubic-bezier(0, 0, 0.2, 1)
- Hover states
- Quick interactive feedback
- Staggered list items & pills
- Buttons & tooltips appearing
cubic-bezier(0.4, 1.4, 0.2, 1.0)
- Success states & confirmations
- Badges & chips appearing
- Scale and translate only
- Positive moments only
Duration Tiers
Consistent timing creates predictable, polished animations.
| Token | Duration | Usage |
|---|---|---|
duration-xs |
100ms | Stagger delays only. Not for standalone transitions. |
duration-sm |
200ms | Hover states, colour/border changes, simple state feedback. |
duration-md |
300ms | Standard fades, small movements, tooltip appearances, button loading states. |
duration-lg |
400ms | Modal/dialog entrances and exits, drawer slides, progress indicators. |
duration-xl |
600ms | Accordions, content expansions with height, multi-property animations. |
duration-xxl |
1000ms | Full-page and viewport-scale transitions. |
Examples
Live animations using the tokens above. Each loops when scrolled into view and pauses when out of view.
ease-slowdown · duration-sm| Token | Value | Used for |
|---|---|---|
ease-slowdown | cubic-bezier(0, 0, 0.2, 1) | Focus ring transition · cursor glide |
duration-sm | 200ms | Border colour + box-shadow on focus |
Timing
- 0msCursor appears above form card
- 200msGlides to "Email address" field (480ms, ease-slowdown)
- 780msField focused — border darkens, focus ring appears, caret blinks
- 2600msGlides to "Password" field (420ms, ease-slowdown)
- 3120msPassword focused; email blurs
- 4900msCursor exits stage right (380ms)
- 5050msPassword blurs
- 6300msLoop
ease-smooth · duration-lg → ease-smooth · duration-md| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Slide in · slide out (translateX) |
duration-lg | 400ms | Entrance |
duration-md | 300ms | Exit |
Timing
- 0msSidebar slides in from left (translateX −100%→0, 400ms, ease-smooth)
- 200msNav content fades in with slight translateX offset (200ms, ease-slowdown)
- 400msSidebar fully visible; content area has shifted right
- 3200msSidebar slides out to left (300ms, ease-smooth)
- 3500msHidden; content area resets
- 5000msLoop (CSS animation)
ease-smooth · duration-lg → ease-smooth · duration-md| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Slide up + fade in · slide down + fade out |
duration-lg | 400ms | Entrance |
duration-md | 300ms | Exit |
Timing
- 0msDialog enters from below (translateY 20px→0, opacity 0→1, 400ms, ease-smooth) + scrim fades in
- 400msDialog fully visible
- 3200msDialog exits down (translateY 0→8px, opacity 1→0, 300ms, ease-smooth) + scrim fades out
- 3500msHidden
- 5000msLoop (CSS animation)
ease-slowdown · duration-md · 100ms delay increments| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Card entrance · heading fade in |
ease-slowdown | cubic-bezier(0, 0, 0.2, 1) | Each pill entrance (scale + opacity) |
ease-standard | cubic-bezier(0.4, 0, 0.2, 1) | Card exit |
duration-lg | 400ms | Card slide in |
duration-md | 300ms | Each pill · card exit |
| 100ms stagger | — | Delay per pill (index × 100ms) |
Timing
- 0msCard slides up (translateY 16px→0, opacity 0→1, 400ms, ease-smooth)
- 200ms"Tags" heading fades in (200ms, ease-smooth)
- 400msPill 1 enters (scale 0.92→1, opacity 0→1, 300ms, ease-slowdown)
- 500msPill 2 enters
- …Each subsequent pill 100ms after previous
- 1200msPill 9 (last) starts entering
- 1500msAll pills visible; hold for 1500ms
- 3000msCard exits (translateY 0→8px, opacity 0, 300ms, ease-standard)
- 4200msLoop
ease-smooth · duration-xxl · Lottie tick| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Page 1 & 2 enter/exit (translateX) |
ease-slowdown | cubic-bezier(0, 0, 0.2, 1) | Cursor glide · button hover transition |
ease-standard | cubic-bezier(0.4, 0, 0.2, 1) | Page 2 fade out |
duration-xxl | 1000ms | Page enter/exit animations |
| 200ms | — | Button background on hover |
Timing
- 0msPage 1 enters (translateX 96px→0, 1000ms, ease-smooth)
- 1300msCursor appears top-right of stage
- 1500msCursor glides to "Continue →" button (550ms, ease-slowdown)
- 1800msButton hover: background → #374151 (200ms, ease-slowdown)
- 2100msCursor presses (scale 0.82, 80ms)
- 2200msButton enters loading state — label swaps for spinner
- 2450msCursor fades out (300ms)
- 3200msPage 1 exits left (translateX 0→−96px, 1000ms, ease-smooth)
- 3700msPage 2 enters (1000ms, ease-smooth)
- 3900msLottie tick plays
- 7400msPage 2 fades out (400ms, ease-standard)
- 8400msLoop
ease-smooth · duration-lg → ease-smooth · duration-md| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Drawer slide up · slide down (translateY) |
duration-lg | 400ms | Entrance |
duration-md | 300ms | Exit |
Timing
- 0msNav tab tap — icon briefly scales down (100ms press, ease-in) and springs back
- 0msScrim fades in (opacity 0→1, 400ms) · Drawer slides up (translateY 100%→0, 400ms, ease-smooth)
- 400msDrawer fully visible; hold for ~2.8s
- 3200msDrawer slides down (translateY 0→100%, 300ms, ease-smooth) · scrim fades out
- 3500msHidden
- 5000msLoop (CSS animation)
ease-smooth · duration-xl → ease-smooth · duration-lg| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Height expand · collapse |
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Chevron rotation |
ease-slowdown | cubic-bezier(0, 0, 0.2, 1) | Content fade in (opacity) |
ease-accelerate | cubic-bezier(1, 0, 1, 1) | Content fade out (opacity) |
duration-xl | 600ms | Entrance — height + chevron |
duration-lg | 400ms | Exit — height + chevron |
duration-md | 300ms | Content fade in · 80ms delay after height starts |
duration-sm | 200ms | Content fade out · simultaneous with collapse |
Timing
- 0msAll items closed
- 600msItem 1 opens — height expands (600ms, ease-smooth) · chevron rotates 180° · content fades in (300ms, ease-slowdown, 80ms delay)
- 2800msItem 1 closes — content fades out (180ms) · height collapses (400ms, ease-smooth) · chevron reverses
- 3800msItem 2 opens
- 5800msItem 2 closes
- 7000msLoop
ease-smooth · duration-md → ease-accelerate · duration-sm| Token | Value | Used for |
|---|---|---|
ease-smooth | cubic-bezier(0.5, 0, 0, 1) | Entrance — slide from right + fade in |
ease-accelerate | cubic-bezier(1, 0, 1, 1) | Exit — slide to right + fade out |
duration-md | 300ms | Entrance |
duration-sm | 200ms | Exit — faster feels like a dismissal, not a departure |
Timing
- 0msAll toasts off-screen right
- 600msToast 1 (success) slides in from right (300ms, ease-smooth)
- 3400msToast 1 exits to right (200ms, ease-accelerate)
- 4200msToast 2 (info) slides in
- 7000msToast 2 exits
- 8000msLoop
Notes
- Exit uses
ease-accelerate— the toast should feel dismissed, not elegantly departing - If a new toast arrives while one is visible, push existing toasts down with
duration-sm/ease-smoothtranslate
Playground
Combine any easing curve with any duration and hit Play to see how the pairing feels.