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
- 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.
- Short. Nothing over 260ms in product UI. Exits run shorter than entrances, so dismissing never waits.
- 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.
- 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). - 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).
- 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
| Token | Value | Use |
|---|---|---|
--motion-fast | 120ms | Popup and working-line exits; the fade a keyboard close plays |
--motion-exit | 150ms | Exits of dialogs and side panels (exits run shorter than entrances) |
--motion-base | 160ms | Hover, selection and focus colour; press; popup entrances; a row leaving |
--motion-move | 200ms | Something already on screen that moves: the sidebar fold, rows sliding into place, expand and collapse, dialog entrances, a card arriving |
--motion-sheet | 250ms | A side sheet sliding in |
--motion-morph | 260ms | The 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
| Token | Curve | Value | Use |
|---|---|---|---|
--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-drawer | cubic-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-drawerPatterns
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
- 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
- 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.
Popup
- 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-basein,--motion-fastout,--ease-out-strong. Tooltips usemotion-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
- 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-movein,--motion-exitout,--ease-out-strong. - Reduced motion: fade only.
- Used by: Modal dialog, Confirm dialog, Keyboard shortcuts sheet.
Sheet
- 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 0is the right edge). - Tokens:
motion-sheet:--motion-sheetin, 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-crmui/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 carrydata-list-rowand their own name in--list-row.--motion-baseout,--motion-movein 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
- 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-heightor--accordion-panel-height. - Reduced motion: at once.
- Used by: Accordion, Group row.
Loading
- 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-pulseon Skeleton;motion-loading-sweepfor 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
- 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)andleave(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.
| Motion | Why it earns one | Page |
|---|---|---|
| Ask bar morph into the composer and back | Shows where the composer came from | Ask bar |
| Send | Shows the message was sent and where it went | Flo thread |
| Flo panel open, close and resize | Shows where Flo lives while the page stays usable | Side panel |
| Sidebar fold and unfold | Shows where the navigation went | Sidebar |
| Flo working shimmer | Shows progress while you wait | Flo message |
| Approval acted on, then Undo | Shows the change happened and can be undone | Flo approval |
Do and don't
| Do | Don't |
|---|---|
Name a pattern: motion-popup | Write 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 click | Snap a keyboard open and close while the pointer animates |
| Fade under reduced motion | Remove the fade too, so an open popup just appears |
| Keep high-frequency UI on a small pattern | Give hover, typing or menus a signature motion |
| Transition named properties | Use 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, somotion-popupuses 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.