Motion design system
Motion tokens you tune, preview, and own.
Pick the kind of product you're building, nudge 3 sliders, and watch a real app move. Then copy one prompt into Claude or Cursor, and your app moves the same way.
- Inbox
- Projects
- Team
- Alerts
- Billing
Settings
Shared with you
Activity
- Maya turned on email digests2m ago
- Leo invited 3 people to Team18m ago
- Ana moved Q4 plan to Projects1h ago
- Sam changed the billing email3h ago
A live app, not a video. Click anything to try it.
- settles 383 ms
- overshoot 0.3%
- travel 15 px
01 · The kit
11 interactions, one personality
Every card runs on the knobs above. Point at one to watch it, or open it for the recipe, the tokens, and the code.
- Press
Scales down on pointer down, springs back on release.
- Toggle
The thumb slides and the track color tweens.
- Overlay
The backdrop fades in. The panel scales up and rises.
- Notify
Slides in from the bottom edge. Fades and drops on exit.
- Reveal
Children fade and rise, one after another.
- Switch
The active indicator slides. Panel content crossfades.
- Menu
Grows out of its trigger. Items fade in one after another.
- Tooltip
Fades in and rises a few pixels. Never pulls the eye.
- Drawer
Slides in from its edge over a fading backdrop.
- Expand
The height springs open. The content fades in.
- Lift
Rises on hover while a shadow fades in.
02 · 7 personalities
Same dialog, 7 feels
Each preset is a saved set of knobs and a style. Play them side by side, then pick one for this site.
Calm
504 ms · 0.0%Docs, finance, healthcare. Glides and comes into focus. Never bounces.
32 frames at 60 fps. Settles in 504 ms, no overshoot. Snappy
383 ms · 0.3%Productivity tools. Quick, with a hair of overshoot.
24 frames at 60 fps. Settles in 383 ms, overshoots 0.3%. Playful
493 ms · 7.2%Consumer and onboarding. Pops, bounces, and squishes.
31 frames at 60 fps. Settles in 493 ms, overshoots 7.1%. Minimal
408 ms · 0.0%Quiet utilities and settings. Tiny moves, a soft glow, nothing extra.
26 frames at 60 fps. Settles in 408 ms, no overshoot. Crisp
312 ms · 0.0%Developer tools and dashboards. Fast and exact. Unfolds, never bounces.
20 frames at 60 fps. Settles in 312 ms, no overshoot. Cinematic
536 ms · 0.0%Media, portfolios, and launch pages. Long, smooth slides that sink on press.
34 frames at 60 fps. Settles in 536 ms, no overshoot. Bouncy
755 ms · 21.0%Games and celebrations. Big pops from where you click, with lots of wobble.
47 frames at 60 fps. Settles in 755 ms, overshoots 20.8%. Make your own
Mix an entrance, a curve, and a press, nudge the knobs, or press Shuffle.
Open the playground
03 · Export
One tune, four files you own
No runtime package and no lock-in. The files below are generated from the knobs you set above, right now.
Not sure what to do with these? The guide walks you through Claude.ai, Claude Code, Cursor, Lovable, v0, and Bolt, step by step.
Open the guide/*
* Motif v0.1.0 · preset: Snappy
* knobs: energy 0.70, bounce 0.20, distance 0.40
* style: entrance rise, curve spring, groups cascade, press squeeze
* Re-open in the playground: https://motifui.dev/playground?e=0.70&b=0.20&d=0.40&en=rise&cu=spring&gr=cascade&pr=squeeze
* Generated file. Change the knobs and export again; do not edit values by hand.
*/
:root {
/* Springs: linear() easing + duration */
--motif-spring-fast: linear(0, 0.023, 0.079, 0.153, 0.236, 0.321, 0.404, 0.482, 0.553, 0.617, 0.674, 0.724, 0.767, 0.804, 0.836, 0.863, 0.886, 0.906, 0.922, 0.936, 0.947, 0.956, 0.964, 0.971, 0.976, 0.98, 0.984, 0.987, 0.989, 0.991, 0.993, 0.994, 0.995, 0.996, 0.997, 0.998, 0.998, 0.998, 0.999, 1);
--motif-spring-fast-duration: 336ms;
--motif-spring-default: linear(0, 0.022, 0.076, 0.15, 0.235, 0.323, 0.41, 0.493, 0.569, 0.638, 0.699, 0.753, 0.799, 0.839, 0.872, 0.899, 0.922, 0.94, 0.955, 0.967, 0.977, 0.984, 0.989, 0.994, 0.997, 0.999, 1, 1.001, 1.002, 1.002, 1.003, 1.003, 1.002, 1.002, 1.002, 1.002, 1.002, 1.001, 1.001, 1);
--motif-spring-default-duration: 383ms;
--motif-spring-slow: linear(0, 0.021, 0.074, 0.146, 0.228, 0.314, 0.399, 0.48, 0.556, 0.624, 0.685, 0.739, 0.786, 0.826, 0.859, 0.888, 0.912, 0.931, 0.947, 0.96, 0.97, 0.978, 0.984, 0.989, 0.993, 0.996, 0.998, 0.999, 1, 1.001, 1.002, 1.002, 1.002, 1.002, 1.002, 1.002, 1.001, 1.001, 1.001, 1);
--motif-spring-slow-duration: 508ms;
/* Tweens: opacity, color */
--motif-duration-instant: 80ms;
--motif-duration-fast: 132ms;
--motif-duration-base: 188ms;
--motif-duration-slow: 282ms;
--motif-duration-move: 376ms;
--motif-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--motif-ease-enter: cubic-bezier(0, 0, 0, 1);
--motif-ease-exit: cubic-bezier(0.3, 0, 1, 1);
--motif-ease-glide: cubic-bezier(0.16, 1, 0.3, 1);
--motif-ease-crisp: cubic-bezier(0.7, 0, 0.3, 1);
/* Travel, scale, and blur */
--motif-distance-xs: 2px;
--motif-distance-sm: 5px;
--motif-distance-md: 15px;
--motif-distance-lg: 30px;
--motif-scale-press: 0.964;
--motif-scale-enter: 0.96;
--motif-scale-pop: 0.71;
--motif-scale-stretch: 1.043;
--motif-blur-sm: 3px;
--motif-blur-md: 9px;
/* Stagger */
--motif-stagger-list: 32ms;
--motif-stagger-cap: 8;
/* Groups: cascade. Children wait --motif-stagger-step x their place. */
--motif-stagger-step: var(--motif-stagger-list);
/* Press: squeeze */
--motif-press-transform: scale(var(--motif-scale-press));
--motif-press-filter: none;
--motif-press-shadow: none;
--motif-press-layer: 0;
--motif-press-layer-active: 0;
}
/* Browsers without linear(): a fitted cubic-bezier and duration. */
@supports not (transition-timing-function: linear(0, 1)) {
:root {
--motif-spring-fast: cubic-bezier(0.2, 0.11, 0.2, 1);
--motif-spring-fast-duration: 244ms;
--motif-spring-default: cubic-bezier(0.28, 0.19, 0.32, 0.94);
--motif-spring-default-duration: 218ms;
--motif-spring-slow: cubic-bezier(0.29, 0.19, 0.24, 0.89);
--motif-spring-slow-duration: 302ms;
}
}
/* press: Every clickable element. Scales to scale.press on pointer down, back to 1 on release.
Use: <button class="motif-press"> */
.motif-press {
transition:
transform var(--motif-spring-fast-duration) var(--motif-spring-fast),
filter var(--motif-duration-fast) var(--motif-ease-standard);
}
.motif-press:active {
transform: var(--motif-press-transform);
filter: var(--motif-press-filter);
}
@layer base {
.motif-press {
position: relative;
}
.motif-press::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: var(--motif-press-shadow);
opacity: var(--motif-press-layer);
pointer-events: none;
transition: opacity var(--motif-duration-fast) var(--motif-ease-standard);
}
.motif-press:active::after {
opacity: var(--motif-press-layer-active);
}
}
/* toggle: Switches and checkboxes. The thumb slides and the track color tweens.
Use: .motif-toggle on the track (role="switch" aria-checked), .motif-toggle-thumb on the thumb.
The track is 2 thumbs wide, so the thumb travels 100% of its own width. */
.motif-toggle {
transition: background-color var(--motif-duration-fast) var(--motif-ease-standard);
}
.motif-toggle-thumb {
transition: transform var(--motif-spring-fast-duration) var(--motif-spring-fast);
}
.motif-toggle[aria-checked="true"] .motif-toggle-thumb {
transform: translateX(100%);
}
/* overlay: Dialogs and alert dialogs. The backdrop fades in. The panel scales up from scale.enter and rises distance.md. Exit reverses, faster.
Use: .motif-overlay-backdrop and .motif-overlay-panel. They enter when mounted.
To exit, set data-state="closed" and unmount after the transition (e.g. Radix forceMount).
To grow it out of the clicked element, set --motif-origin on the panel (e.g. "40px 120px"). */
.motif-overlay-backdrop {
transition: opacity var(--motif-duration-base) var(--motif-ease-enter);
}
@starting-style {
.motif-overlay-backdrop {
opacity: 0;
}
}
.motif-overlay-backdrop[data-state="closed"] {
opacity: 0;
transition: opacity var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-overlay-panel {
transform-origin: var(--motif-origin, center);
transition:
opacity var(--motif-duration-base) var(--motif-ease-enter),
transform var(--motif-spring-default-duration) var(--motif-spring-default),
filter var(--motif-duration-base) var(--motif-ease-enter),
clip-path var(--motif-spring-default-duration) var(--motif-spring-default);
}
@starting-style {
.motif-overlay-panel {
opacity: 0;
transform: translateY(var(--motif-distance-md)) scale(var(--motif-scale-enter));
filter: none;
clip-path: none;
}
}
.motif-overlay-panel[data-state="closed"] {
opacity: 0;
transform: translateY(var(--motif-distance-md)) scale(var(--motif-scale-enter));
filter: none;
clip-path: none;
transition:
opacity var(--motif-duration-fast) var(--motif-ease-exit),
transform var(--motif-duration-fast) var(--motif-ease-exit),
filter var(--motif-duration-fast) var(--motif-ease-exit),
clip-path var(--motif-duration-fast) var(--motif-ease-exit);
}
/* notify: Toasts and banners. Slides in distance.lg from the bottom edge and fades in. Exit fades and drops distance.sm.
Use: <div class="motif-notify" role="status">. Exit with data-state="closed". */
.motif-notify {
transition:
opacity var(--motif-duration-fast) var(--motif-ease-enter),
transform var(--motif-spring-default-duration) var(--motif-spring-default),
filter var(--motif-duration-fast) var(--motif-ease-enter),
clip-path var(--motif-spring-default-duration) var(--motif-spring-default);
}
@starting-style {
.motif-notify {
opacity: 0;
transform: translateY(var(--motif-distance-lg));
filter: none;
clip-path: none;
}
}
.motif-notify[data-state="closed"] {
opacity: 0;
transform: translateY(var(--motif-distance-sm));
filter: none;
clip-path: none;
transition:
opacity var(--motif-duration-fast) var(--motif-ease-exit),
transform var(--motif-duration-fast) var(--motif-ease-exit),
filter var(--motif-duration-fast) var(--motif-ease-exit),
clip-path var(--motif-duration-fast) var(--motif-ease-exit);
}
/* reveal: Lists, grids, search results. Children fade in and rise distance.md, one after another, up to stagger.cap.
Use: <ul class="motif-reveal">. Children enter when mounted. */
.motif-reveal > * {
transition:
opacity var(--motif-duration-base) var(--motif-ease-enter),
transform var(--motif-spring-default-duration) var(--motif-spring-default),
filter var(--motif-duration-base) var(--motif-ease-enter),
clip-path var(--motif-spring-default-duration) var(--motif-spring-default);
}
@starting-style {
.motif-reveal > * {
opacity: 0;
transform: translateY(var(--motif-distance-md));
filter: none;
clip-path: none;
}
}
.motif-reveal > * {
transition-delay: calc(var(--motif-stagger-step) * var(--motif-stagger-n, 0));
}
.motif-reveal > :nth-child(2) {
--motif-stagger-n: 1;
}
.motif-reveal > :nth-child(3) {
--motif-stagger-n: 2;
}
.motif-reveal > :nth-child(4) {
--motif-stagger-n: 3;
}
.motif-reveal > :nth-child(5) {
--motif-stagger-n: 4;
}
.motif-reveal > :nth-child(6) {
--motif-stagger-n: 5;
}
.motif-reveal > :nth-child(7) {
--motif-stagger-n: 6;
}
.motif-reveal > :nth-child(8) {
--motif-stagger-n: 7;
}
/* Children past stagger.cap (8) enter with the last staggered child. */
.motif-reveal > :nth-child(n + 9) {
--motif-stagger-n: 7;
}
/* switch: Tabs and segmented controls. The active indicator slides. Panel content crossfades with a distance.sm shift.
Use: .motif-switch-indicator (set its transform and width to the active tab from JS),
.motif-switch-panel on the panel content (re-mount it on tab change). */
.motif-switch-indicator {
transition:
transform var(--motif-spring-default-duration) var(--motif-spring-default),
width var(--motif-spring-default-duration) var(--motif-spring-default);
}
.motif-switch-panel {
transition:
opacity var(--motif-duration-fast) var(--motif-ease-enter),
transform var(--motif-spring-default-duration) var(--motif-spring-default);
}
@starting-style {
.motif-switch-panel {
opacity: 0;
transform: translateX(var(--motif-distance-sm));
}
}
/* menu: Dropdown menus and popovers. The menu scales up from scale.enter at its trigger and drops distance.sm into place. Items fade in one after another, up to stagger.cap.
Use: .motif-menu on the menu (set --motif-origin to the trigger's corner or the clicked point),
.motif-menu-item on each item. Exit with data-state="closed". */
.motif-menu {
transform-origin: var(--motif-origin, top left);
transition:
opacity var(--motif-duration-fast) var(--motif-ease-enter),
transform var(--motif-spring-fast-duration) var(--motif-spring-fast),
filter var(--motif-duration-fast) var(--motif-ease-enter),
clip-path var(--motif-spring-fast-duration) var(--motif-spring-fast);
}
@starting-style {
.motif-menu {
opacity: 0;
transform: translateY(calc(-1 * var(--motif-distance-sm))) scale(var(--motif-scale-enter));
filter: none;
clip-path: none;
}
}
.motif-menu[data-state="closed"] {
opacity: 0;
transform: scale(var(--motif-scale-enter));
filter: none;
clip-path: none;
transition:
opacity var(--motif-duration-fast) var(--motif-ease-exit),
transform var(--motif-duration-fast) var(--motif-ease-exit),
filter var(--motif-duration-fast) var(--motif-ease-exit),
clip-path var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-menu-item {
transition: opacity var(--motif-duration-fast) var(--motif-ease-enter);
}
@starting-style {
.motif-menu-item {
opacity: 0;
}
}
.motif-menu-item {
transition-delay: calc(var(--motif-stagger-step) * var(--motif-stagger-n, 0));
}
.motif-menu-item:nth-child(2) {
--motif-stagger-n: 1;
}
.motif-menu-item:nth-child(3) {
--motif-stagger-n: 2;
}
.motif-menu-item:nth-child(4) {
--motif-stagger-n: 3;
}
.motif-menu-item:nth-child(5) {
--motif-stagger-n: 4;
}
.motif-menu-item:nth-child(6) {
--motif-stagger-n: 5;
}
.motif-menu-item:nth-child(7) {
--motif-stagger-n: 6;
}
.motif-menu-item:nth-child(8) {
--motif-stagger-n: 7;
}
/* Children past stagger.cap (8) enter with the last staggered child. */
.motif-menu-item:nth-child(n + 9) {
--motif-stagger-n: 7;
}
/* tooltip: Tooltips and hints. Fades in and rises distance.sm. The smallest travel in the kit, so it never pulls the eye from the pointer.
Use: <div class="motif-tooltip" role="tooltip">. Exit with data-state="closed". */
.motif-tooltip {
transition:
opacity var(--motif-duration-fast) var(--motif-ease-enter),
transform var(--motif-spring-fast-duration) var(--motif-spring-fast),
filter var(--motif-duration-fast) var(--motif-ease-enter),
clip-path var(--motif-spring-fast-duration) var(--motif-spring-fast);
}
@starting-style {
.motif-tooltip {
opacity: 0;
transform: translateY(var(--motif-distance-sm));
filter: none;
clip-path: none;
}
}
.motif-tooltip[data-state="closed"] {
opacity: 0;
transform: none;
filter: none;
clip-path: none;
transition:
opacity var(--motif-duration-fast) var(--motif-ease-exit),
transform var(--motif-duration-fast) var(--motif-ease-exit),
filter var(--motif-duration-fast) var(--motif-ease-exit),
clip-path var(--motif-duration-fast) var(--motif-ease-exit);
}
/* drawer: Side panels and mobile navigation. The backdrop fades in. The panel slides in from its edge. Exit slides it back out, faster.
Use: .motif-drawer-backdrop and .motif-drawer-panel (right edge; add data-side="left" for the left edge).
They enter when mounted. To exit, set data-state="closed" and unmount after the transition.
Bouncy presets overshoot: let the panel run past its edge, so the overshoot never opens a gap. */
.motif-drawer-backdrop {
transition: opacity var(--motif-duration-base) var(--motif-ease-enter);
}
.motif-drawer-panel {
transition: transform var(--motif-spring-default-duration) var(--motif-spring-default);
}
@starting-style {
.motif-drawer-backdrop {
opacity: 0;
}
}
@starting-style {
.motif-drawer-panel {
transform: translateX(100%);
}
}
@starting-style {
.motif-drawer-panel[data-side="left"] {
transform: translateX(-100%);
}
}
.motif-drawer-backdrop[data-state="closed"] {
opacity: 0;
transition: opacity var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-drawer-panel[data-state="closed"] {
transform: translateX(100%);
transition: transform var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-drawer-panel[data-side="left"][data-state="closed"] {
transform: translateX(-100%);
}
/* expand: Accordions and disclosures. The panel's height springs open. Its content fades in. Closing tweens the height shut, faster.
Use: <div class="motif-expand" data-state="open|closed"> with one child that holds the content.
The height animates through grid-template-rows (0fr to 1fr), so no height is measured. */
.motif-expand {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-expand > * {
min-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity var(--motif-duration-fast) var(--motif-ease-exit);
}
.motif-expand[data-state="open"] {
grid-template-rows: 1fr;
transition: grid-template-rows var(--motif-spring-default-duration) var(--motif-spring-default);
}
.motif-expand[data-state="open"] > * {
opacity: 1;
transition: opacity var(--motif-duration-fast) var(--motif-ease-standard);
}
/* lift: Hoverable cards and tiles. The card rises distance.sm on hover and its shadow fades in. It settles back when the pointer leaves.
Use: <a class="motif-lift">. The shadow is a layer that fades, so only opacity and transform animate. */
.motif-lift {
position: relative;
transition: transform var(--motif-spring-fast-duration) var(--motif-spring-fast);
}
.motif-lift::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 8px 24px -6px rgb(0 0 0 / 0.25);
opacity: 0;
pointer-events: none;
transition: opacity var(--motif-duration-fast) var(--motif-ease-standard);
}
@media (hover: hover) {
.motif-lift:hover {
transform: translateY(calc(-1 * var(--motif-distance-sm)));
}
.motif-lift:hover::after {
opacity: 1;
}
}
.motif-lift:focus-visible {
transform: translateY(calc(-1 * var(--motif-distance-sm)));
}
.motif-lift:focus-visible::after {
opacity: 1;
}
/* Reduced motion: no travel, no scale, no blur, no stagger. Movement is instant.
Opacity fades stay, at duration.fast, so state changes stay visible. */
@media (prefers-reduced-motion: reduce) {
:root {
--motif-spring-fast-duration: 0ms;
--motif-spring-default-duration: 0ms;
--motif-spring-slow-duration: 0ms;
--motif-duration-base: 132ms;
--motif-duration-slow: 132ms;
--motif-distance-xs: 0px;
--motif-distance-sm: 0px;
--motif-distance-md: 0px;
--motif-distance-lg: 0px;
--motif-scale-press: 1;
--motif-scale-enter: 1;
--motif-scale-pop: 1;
--motif-scale-stretch: 1;
--motif-blur-sm: 0px;
--motif-blur-md: 0px;
--motif-stagger-list: 0ms;
--motif-stagger-step: 0ms;
--motif-press-filter: brightness(0.9);
}
/* drawer: the panel does not slide. It appears in place and fades. */
.motif-drawer-panel,
.motif-drawer-panel[data-state],
.motif-drawer-panel[data-side][data-state] {
transform: none;
transition: opacity var(--motif-duration-fast) var(--motif-ease-standard);
}
@starting-style {
.motif-drawer-panel {
opacity: 0;
}
}
.motif-drawer-panel[data-state="closed"] {
opacity: 0;
}
/* expand: the panel opens and closes to full height at once. The content still fades. */
.motif-expand,
.motif-expand[data-state] {
transition: none;
}
}
04 · Accessible by default
Reduced motion is designed, not switched off
Every recipe and every export carries its own reduced-motion version. When someone asks their system for less motion, Motif follows three rules.
- 1No travel and no scale. Every distance becomes 0 and every scale becomes 1.
- 2Fades stay, at duration.fast, so every change is still visible.
- 3Focus, roles, and live regions never wait for an animation.
Find your app's motion personality.
Six picks, about 30 seconds. Then tune and export.