Foundations

Motion

Short and functional, copied from the app's code. Six durations, three curves, ten shared patterns that every component names, and a few signature motions on their own component pages.

Every value on this page is read from hire-crm src/app/globals.css, src/components/ui/* and src/components/flo/send-motion.ts at 89ed03870. The tokens and patterns live in app/global.css; the script half lives in @/ui/motion. Each demo below runs that CSS, so what loops here is what ships. Press Reduced motion under any demo to see its fallback.

Principles

  1. Motion explains a change. It shows where something came from (the composer grows out of the Ask bar), what caused a change (the message moves from the box into the thread), or that work is still going (the shimmer). If it explains nothing, it doesn't move.
  2. Short. Nothing over 260ms in product UI. Exits run shorter than entrances, so dismissing never waits.
  3. One curve per job. Things that enter use the strong ease-out. Things already on screen that move use the strong ease-in-out. Drawers and the Ask bar morph use the iOS sheet curve.
  4. Transform and opacity. Colour fades too, never transition-all. Two motions change size because the app does: the sidebar's width (the page beside it has to reflow) and the Ask bar morph (the box grows into the composer).
  5. Keys animate like the click they replace. ⌘B, ⌘K, Esc and the arrow keys play the same 120 to 160ms motion as the pointer (C708, Joseph 13 Oct 2026).
  6. Reduced motion keeps fades only. Nothing slides, grows or presses in. A fade still says that something opened.

Pattern first, signature rarely

A component names one of the shared patterns below and links to it from its Motion section. It gets a signature motion of its own only when the motion explains a cause, an origin or progress and people don't trigger it many times a minute. Anything used dozens of times a day (hover, selection, typing, menus) stays on a pattern, and the pattern stays small (Emil Kowalski, You don't need animations). This is how Atlassian, Carbon, Material 3 and Fluent 2 split motion: a few named transitions for everything, and component-specific motion only for a short list.

Durations

TokenValueUse
--motion-fast120msPopup and working-line exits; the fade a keyboard close plays
--motion-exit150msExits of dialogs and side panels (exits run shorter than entrances)
--motion-base160msHover, selection and focus colour; press; popup entrances; a row leaving
--motion-move200msSomething already on screen that moves: the sidebar fold, rows sliding into place, expand and collapse, dialog entrances, a card arriving
--motion-sheet250msA side sheet sliding in
--motion-morph260msThe Ask bar morph and Send, the longest motion in the product

Use them as duration-(--motion-base), or name a pattern. Never write a raw ms in a component.

Easing

TokenCurveValueUse
--ease-out-strong (--ease-out)cubic-bezier(0.23, 1, 0.32, 1)Anything that enters or leaves: popups, dialogs, toasts, fades. Also the default curve for every transition-* utility
--ease-in-out-strong (--ease-in-out)cubic-bezier(0.77, 0, 0.175, 1)Something on screen that changes size or place: the sidebar fold
--ease-drawercubic-bezier(0.32, 0.72, 0, 1)Sheets, rows sliding into place, the Ask bar morph and Send

There is no ease-in: an exit on ease-in starts slowly and feels late. The same three curves at 250ms:

--ease-out-strong
--ease-in-out-strong
--ease-drawer
Loops every 2.8s

Patterns

Each pattern is a class (motion-popup) or a helper in @/ui/motion. It bundles the duration, the curve, what moves and the reduced-motion fallback, as Atlassian's semantic motion tokens do.

Press

Loops every 2.8s
  • Use when: a button, chip or row is pressed, by pointer, Enter or Space.
  • Moves: scale to 0.97 while held, with the State change colours. Not on a menu trigger (aria-haspopup), which opens a popup instead.
  • Tokens: motion-press: --motion-base, --ease-out-strong. The sidebar's items and the Ask bar pill press to 0.98, as in the app.
  • Reduced motion: colour only.
  • Used by: Button, Ask bar, Flo composer Send and attach, Sidebar items.

State change

Loops every 2.8s
  • Use when: hover, selection or focus changes a fill, a colour or an edge.
  • Moves: colour, background, border, shadow and outline colour. Nothing else.
  • Tokens: motion-state: --motion-base, --ease-out-strong.
  • Reduced motion: unchanged; a colour fade doesn't move anything.
  • Used by: Button, Link, Tag, Record chip, Attachment chip.
Long list · Short list · Interview
Loops every 2.8s
  • Use when: a menu, popover, select, picker, hover card or tooltip opens from a trigger.
  • Moves: fades and grows from 0.97 at the trigger (transform-origin), in 160ms, out 120ms.
  • Tokens: motion-popup: --motion-base in, --motion-fast out, --ease-out-strong. Tooltips use motion-tooltip: 125ms both ways after the provider's 300ms delay; moving to the next trigger skips the delay.
  • Reduced motion: fade only.
  • Used by: Popover, Tooltip, Dropdown menu.

Dialog

Delete Northwind?

Its 4 jobs and 12 applications go too.

Loops every 2.8s
  • Use when: a modal or confirm dialog opens over the page.
  • Moves: fades and grows from 0.95, in 200ms, out 150ms.
  • Tokens: motion-dialog: --motion-move in, --motion-exit out, --ease-out-strong.
  • Reduced motion: fade only.
  • Used by: Modal dialog, Confirm dialog, Keyboard shortcuts sheet.

Sheet

Ada Lovelace

Interview · Northwind

Loops every 2.8s
  • Use when: a side sheet or record peek slides in from an edge.
  • Moves: 40px in from its edge and fades, in 250ms, out 180ms; the scrim fades in 150ms. Set the edge with --motion-sheet-from (2.5rem 0 is the right edge).
  • Tokens: motion-sheet: --motion-sheet in, 180ms out (the app's own value), --ease-drawer.
  • Reduced motion: fade only.
  • Used by: Drawer and side sheet, Record peek.

Toast

Replay sends a toast to the corner of the page.

  • Use when: a result or an Undo needs saying without stopping the work.
  • Moves: slides up into the stack and fades over 400ms, leaves in 200ms; the stack's height follows in 150ms.
  • Tokens: the toast's own Base UI recipe on --ease-out-strong (hire-crm ui/toast.tsx).
  • Reduced motion: fade only.
  • Used by: Toast, Undo control.

Row in and out

  • Ada Lovelace
  • Grace Hopper
  • Alan Turing
  • Katherine Johnson
  • Use when: a row leaves a list or comes back, and the rest close up: Today's Needs you, the job board, a tag removed.
  • Moves: the leaving row fades up 4px in 160ms, an arriving row fades in over 200ms, and the rest slide into place in 200ms. It is one view transition.
  • Tokens: moveList(change, list) from @/ui/motion; rows carry data-list-row and their own name in --list-row. --motion-base out, --motion-move in and for the slide, --ease-drawer.
  • Reduced motion: the change happens at once.
  • Used by: Needs you list, Kanban board, Tag group.

Expand and collapse

Interview · 3
Ada Lovelace
Grace Hopper
Alan Turing
Loops every 2.8s
  • Use when: an accordion, a group row or a details panel opens in place.
  • Moves: the panel's height, in 200ms. A toggle in mid-motion reverses from where it is.
  • Tokens: motion-expand: --motion-move, --ease-out-strong. It reads Base UI's --collapsible-panel-height or --accordion-panel-height.
  • Reduced motion: at once.
  • Used by: Accordion, Group row.

Loading

Loops every 2.8s
  • Use when: a wait is still running after 300ms.
  • Moves: a skeleton pulses on a 2s cycle at its final height; a search line fades in after 300ms and sweeps every 1s.
  • Tokens: motion-safe:animate-pulse on Skeleton; motion-loading-sweep for the line. The Flo working line has its own shimmer, a signature motion.
  • Reduced motion: the skeleton holds still; the line shows at half strength without the sweep.
  • Used by: Skeleton, Spinner, Progress bar, Typed object card.

Arrive

Searched candidates
Found 3 matches
Drafted the shortlist
Loops every 2.8s
  • Use when: a card, a step or a status line arrives while a reply streams.
  • Moves: rises 4px (or grows from 0.97, for the working line) and fades in over 200ms. The working line fades out in 120ms before it goes, so a reply never takes its place in the same frame.
  • Tokens: enter(node), enterScaled(node) and leave(node, done) from @/ui/motion: --motion-move, --motion-fast, --ease-out-strong.
  • Reduced motion: fade only.
  • Used by: Flo message, Flo approval, Typed object card.

Choreography

When several things arrive in one commit, each waits 40ms behind the one before it, and none waits more than 160ms (enter in @/ui/motion, copied from the app's send-motion.ts). A whole sequence stays under 500ms, as Carbon's choreography rule says. Carbon staggers 20ms; the app uses 40ms and the library keeps it.

Reduced motion

prefers-reduced-motion: reduce turns every pattern to its fallback: popups, dialogs, sheets and toasts fade without moving, presses keep their colour, the sidebar changes width at once, and view transitions (rows, the morph, Send) apply at once. data-reduce-motion on any element does the same for its subtree, as Fluent 2's no-motion setting does; Tailwind's motion-safe: and motion-reduce: variants and the helpers in @/ui/motion all honour it. The toggles under each demo use it.

Signature motions

These explain something a pattern can't, and none happens many times a minute. Each component page shows the motion live with Replay, a readout of every value and its reason, and do and don't.

MotionWhy it earns onePage
Ask bar morph into the composer and backShows where the composer came fromAsk bar
SendShows the message was sent and where it wentFlo thread
Flo panel open, close and resizeShows where Flo lives while the page stays usableSide panel
Sidebar fold and unfoldShows where the navigation wentSidebar
Flo working shimmerShows progress while you waitFlo message
Approval acted on, then UndoShows the change happened and can be undoneFlo approval

Do and don't

DoDon't
Name a pattern: motion-popupWrite duration-160 ease-out or a raw 150ms in a component
Exit faster than you enter (160 in, 120 out)Use ease-in, or a longer exit than entrance
Animate ⌘K, Esc and ⌘B like the clickSnap a keyboard open and close while the pointer animates
Fade under reduced motionRemove the fade too, so an open popup just appears
Keep high-frequency UI on a small patternGive hover, typing or menus a signature motion
Transition named propertiesUse transition-all

Where the library differs from the app

  • Popup scale. The app's menus, popovers and selects grow from 0.95 (zoom-in-95). ADS-MAP 04 and the Paper Motion page lock 0.97, so motion-popup uses 0.97.
  • ADS-MAP 04 wrote side panels as 250/200 on the drawer curve. In the app the Flo panel's body slides in 200ms and out 150ms on the strong ease-out, and sheets leave in 180ms. This page and the patterns follow the code; ADS-MAP 04 is corrected to match.

Design board

Foundation 04
Motion
Short and functional, copied from the app's code. Six durations, three curves, ten shared patterns. Tokens live in code (app/global.css, copied from hire-crm globals.css @89ed03870); Paper has no duration or easing token type, so this page documents them.
Role
Token
Value
Use
Durations and curves
Duration scale
--motion-*
fast 120 · exit 150 · base 160 · move 200 · sheet 250 · morph 260
Product UI only. Exits run shorter than entrances.
Enter and exit
--ease-out-strong
cubic-bezier(0.23, 1, 0.32, 1)
Both ways. No ease-in: exits stay quick.
Move on screen
--ease-in-out-strong
cubic-bezier(0.77, 0, 0.175, 1)
Something on screen that resizes: the sidebar fold
Panels
--ease-drawer
cubic-bezier(0.32, 0.72, 0, 1)
Sheets, rows sliding into place, the Ask bar morph and Send
Reduced motion
prefers-reduced-motion
Fades stay, movement goes
A fade still tells you something opened
Patterns (motion-press, motion-popup, …; live on the site's Motion page)
Hover and colour
--motion-base
160ms
Colour only, never transition-all
Button press
--motion-base
Scale 0.97 plus the colour change
Click and Enter. Not on menu triggers.
Menus and popovers
--ease-out
160 / 120ms, fade + scale from 0.97
Grows from the trigger (transform-origin)
Dialogs
--ease-out
200 / 150ms, fade + scale from 0.95
With the blanket
Side panels
--ease-drawer · --ease-out-strong
Sheets 250 / 180ms, 40px slide · Flo panel 200 / 150ms
Sheets on the drawer curve; the Flo panel's body on the strong ease-out
Toasts
--ease-out-strong
400 / 200ms, stacked; height 150ms
The app's Base UI toast recipe
Tooltips
TooltipProvider
300ms delay, 125ms fade from 0.97
Set once on the provider. Moving to the next trigger skips the delay.
Keyboard actions
--motion-fast · --motion-base
Animate, 120 to 160ms
Shortcuts, arrow keys, ⌘K. Looks like the click it replaces.
Rules
1. Use the --motion-* durations and the three curves. Nothing over 260ms in product UI.
2. Popups grow from 0.97 at 160 / 120ms; dialogs from 0.95 at 200 / 150ms.
3. One tooltip delay, 300ms, set on TooltipProvider. No per-trigger overrides.
4. Animate colour, opacity and transform only. Never transition-all.

On this page