The design tool for how your interface behaves
Choreograph hover, press, focus and drag states on a canvas that is a real browser. You design the interaction by doing it.
The handoff problem
Designers describe motion in Loom recordings. Engineers reinterpret it by eye. What ships is an approximation of an approximation.
Motion tools think in linear timelines and export artefacts that don’t map to component states or respond to real events. Prototypes show intent and ship nothing.
Code gets it exactly right, and asks designers to think in keyframe syntax to get there. So the interaction is rebuilt by hand after handoff, lossily, every time.
Figma is the source of truth for structure. Choreo is the source of truth for behaviour, so the interaction you hand over is the one that ships.
Scrubbing is the whole idea
Drag a playhead and the component responds in the same frame. No render step, no preview mode. Scroll this section and you are the playhead.
Lock your card
- Card · Scale
- 1.000
- Card · Move Y
- 0.0px
- Header · Opacity
- 1.00
A real template, not a mock-up. In the app this panel follows the playhead you drag; here it follows your scroll. Same keyframes, same numbers.
You design the interaction by doing it
Nothing to declare up front. Choreo's canvas is a real browser, so the thing you drag is the thing your users will touch.
Lock your card
This is a real Choreo file, not a picture of one
The Bank App card template, dropped straight into this page. Hover it, click it to flip between its Base and Clicked states, flip the switch. There is no player in between that can drift from what the browser does.
Arm a trigger
Pick one of seven triggers and the element that fires it. That is the whole setup.
Then just design
Drag it, restyle it, resize it. Every edit lands as a keyframe at the playhead.
- Vanilla CSSstyles.css
- Web Animationsanimation.js
- ReactCard.tsx
- Motion Specspec.md
Copy the output
The code panel has been compiling the whole time. Take any of the four targets.
You already know how to animate. You just don't know the syntax.
There is no Animate tab and no property palette. Arm a trigger, make the change you want, and Choreo writes down what you did.
Hover
Enter and exit are edited apart, so a card can rise fast and settle slow instead of playing one curve backwards.
Recorded
- Move Y0 → −8px240ms
- Shadowsoft → deep240ms
Every trigger is authored the same way. Arm it, make the change you want, and Choreo writes down what you did.
Seven triggers, each owning its own timeline
A linear timeline is the wrong shape for interface motion. The unit of choreography here is a trigger state, and enter and exit are edited apart, so a card can rise fast and settle slow.
.choreo-bank-app-card { position: relative; width: 411px; height: 530px; background-image: linear-gradient(0deg, #f7f5f5 0%, #efefef 100%); box-sizing: border-box; border: 1px solid #ffffff; overflow: hidden; border-radius: 31px;}.choreo-bank-app-card__frame-1686561429 { position: absolute; left: 23px; top: 27px; width: fit-content; height: fit-content; box-sizing: border-box; display: flex; gap: 10px; align-items: center; transform: translateX(0px) translateY(0px); opacity: 1; pointer-events: none;}.choreo-bank-app-card__visa-inc-logo-2005-2014-1 { width: 30px; height: 11px;}.choreo-bank-app-card__visa-inc-logo-2005-2014-1 svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__view-cards-details { width: 125px; height: 17px; color: #171717; font-size: 14px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__7520 { width: fit-content; height: fit-content; color: #969696; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; white-space: nowrap; overflow-wrap: break-word;}.choreo-bank-app-card__card { appearance: none; border: none; margin: 0; padding: 0; font: inherit; text-align: inherit; background: none; color: inherit; position: absolute; left: 22px; top: 20px; width: 367px; height: 210px; box-sizing: border-box; display: flex; flex-direction: column; gap: 72px; padding: 32px 0px; justify-content: center; background-image: radial-gradient(circle, #1b1c23 0%, #32343d 100%); box-shadow: 0px 11px 34px 0px rgba(0, 0, 0, 0.45); overflow: hidden; transform: rotateY(180deg); border-radius: 12px; opacity: 0; cursor: pointer;}.choreo-bank-app-card__frame-1686561438 { position: relative; align-self: stretch; height: 63px; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px; justify-content: flex-end;}.choreo-bank-app-card__frame-1686561432 { position: relative; align-self: stretch; height: 38px; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px; justify-content: center; background-color: #0000004a;}.choreo-bank-app-card__frame-1686561436 { position: relative; align-self: stretch; height: fit-content; box-sizing: border-box; display: flex; gap: 27px; padding: 0px 20px; justify-content: space-between; opacity: 0.5;}.choreo-bank-app-card__frame-1686561433 { position: relative; width: 152px; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px;}.choreo-bank-app-card__jannet-smith { width: 152px; height: 12px; color: #969696; font-size: 10px; font-weight: 600; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__2400-3256-2323-7619 { width: 152px; height: 17px; color: #969696; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__frame-1686561437 { position: relative; width: fit-content; height: fit-content; box-sizing: border-box; display: flex; gap: 27px; align-items: center;}.choreo-bank-app-card__frame-1686561434 { position: relative; width: 67px; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 11px;}.choreo-bank-app-card__exp-date { width: 67px; height: 17px; color: #969696; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__05-35 { width: 67px; height: 17px; color: #969696; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__frame-1686561435 { position: relative; width: 31px; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 11px;}.choreo-bank-app-card__cvc { width: 31px; height: 17px; color: #969696; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__123 { width: 31px; height: 17px; color: #969696; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__card-2 { appearance: none; border: none; margin: 0; padding: 0; font: inherit; text-align: inherit; background: none; color: inherit; position: absolute; left: 90px; top: 80px; width: 227px; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 28px; padding: 12px 16px; justify-content: flex-end; align-items: flex-end; background-image: radial-gradient(circle, #1b1c23 0%, #32343d 100%); box-shadow: 0px 11px 34px 0px rgba(0, 0, 0, 0.45); overflow: hidden; border-radius: 12px; transform: translateX(0px) translateY(0px) scaleX(1) scaleY(1); cursor: pointer;}.choreo-bank-app-card__frame-1686561427 { position: relative; align-self: stretch; height: fit-content; box-sizing: border-box; display: flex; gap: 137px; justify-content: space-between; align-items: center;}.choreo-bank-app-card__bank { width: 41px; height: 17px; color: #ffffff; font-size: 14px; font-weight: 800; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__mynaui-contactless { width: 18px; height: 18px;}.choreo-bank-app-card__mynaui-contactless svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__frame-1686561432-2 { position: relative; align-self: stretch; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px;}.choreo-bank-app-card__uit-sim-card { width: 27px; height: 21px;}.choreo-bank-app-card__uit-sim-card svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__g4158 { width: 30px; height: 11px;}.choreo-bank-app-card__g4158 svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__frame-1686561430 { position: absolute; left: 20px; top: 218px; width: 367px; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 14px; justify-content: center; align-items: center;}.choreo-bank-app-card__card-details { position: relative; width: 367px; height: fit-content; box-sizing: border-box; display: flex; flex-direction: column; gap: 5px; align-items: center;}.choreo-bank-app-card__frame-1686561427-2 { position: relative; align-self: stretch; height: 108px; background-color: #ffffff; overflow: hidden; border-radius: 20px; opacity: 0; display: none;}.choreo-bank-app-card__frame-1686561422 { position: absolute; left: 22px; top: 18px; width: 323px; height: fit-content; box-sizing: border-box; display: flex; gap: 159px; justify-content: space-between; align-items: flex-end;}.choreo-bank-app-card__lock-your-card { margin: 0; width: 114px; height: 19px; color: #979797; font-size: 16px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__switch { appearance: none; border: none; margin: 0; padding: 0; font: inherit; text-align: inherit; background: none; color: inherit; position: relative; width: 51px; height: 24px; box-sizing: border-box; display: flex; gap: 10px; padding: 0px 2px; justify-content: flex-end; align-items: center; border-radius: 30px; transform: translateX(0px) translateY(0px); background-color: #1cc24b; cursor: pointer;}.choreo-bank-app-card__rectangle-9 { position: relative; width: 27px; height: 20px; background-color: #ffffff; transform-origin: 51% 50%; border-radius: 30px; transform: translateX(0px) translateY(0px);}.choreo-bank-app-card__frame-1686561423 { position: absolute; left: 22px; top: 71px; width: 326px; height: 20px; box-sizing: border-box; display: flex; gap: 115px; justify-content: space-between; align-items: center;}.choreo-bank-app-card__what-is-a-virtual-card { width: 162px; height: 19px; color: #979797; font-size: 16px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__wordpress-external { position: relative; width: 20px; height: 20px; overflow: hidden;}.choreo-bank-app-card__wordpress-external-2 { position: absolute; left: 0px; top: 0px; width: 18px; height: 18px;}.choreo-bank-app-card__wordpress-external-2 svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__line-19 { position: absolute; left: 23px; top: 53px; width: 322px; height: 1px; fill: none; stroke: #efefef; stroke-width: 1px;}.choreo-bank-app-card__line-19 svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__line-19 svg, .choreo-bank-app-card__line-19 svg * { stroke: inherit !important; fill: inherit !important; stroke-width: inherit !important;}.choreo-bank-app-card__your-virtual-card-will-have-a-different-number-than-your-physical-card-this-helps-keep-your-information-secure { width: 341px; height: 30px; display: none; color: #b4b4b4; font-size: 12px; font-weight: 500; font-family: 'Inter', sans-serif; text-align: center; overflow-wrap: break-word;}.choreo-bank-app-card__group { position: relative; align-self: stretch; height: 278px; box-sizing: border-box; display: flex; flex-direction: column; gap: 14px;}.choreo-bank-app-card__direct-deposit { width: 367px; height: fit-content; color: #b0afaf; font-size: 13px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__frame-1686561421 { position: relative; align-self: stretch; height: 163px; background-color: #ffffff; overflow: hidden; border-radius: 20px;}.choreo-bank-app-card__frame-1686561422-2 { position: absolute; left: 22px; top: 18px; width: 323px; height: fit-content; box-sizing: border-box; display: flex; gap: 159px; justify-content: space-between; align-items: flex-end;}.choreo-bank-app-card__institution-number { width: 140px; height: 19px; color: #b5b5b5; font-size: 16px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__832 { width: 21px; height: 16px; color: #000000; font-size: 12px; font-weight: 500; font-family: 'Reddit Mono', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__frame-1686561423-2 { position: absolute; left: 22px; top: 71px; width: 323px; height: fit-content; box-sizing: border-box; display: flex; gap: 115px; justify-content: space-between; align-items: flex-end;}.choreo-bank-app-card__transit-branch-number { width: 176px; height: 19px; color: #b5b5b5; font-size: 16px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__0055 { width: 27px; height: 16px; color: #000000; font-size: 12px; font-weight: 500; font-family: 'Reddit Mono', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__frame-1686561424 { position: absolute; left: 22px; top: 126px; width: 323px; height: fit-content; box-sizing: border-box; display: flex; gap: 133px; justify-content: space-between; align-items: flex-end;}.choreo-bank-app-card__account-number { width: 127px; height: 19px; color: #b5b5b5; font-size: 16px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__node { width: fit-content; height: fit-content; color: #000000; font-size: 14px; font-weight: 500; font-family: 'Reddit Mono', sans-serif; white-space: nowrap; overflow-wrap: break-word;}.choreo-bank-app-card__line-19-2 { position: absolute; left: 23px; top: 53px; width: 322px; height: 1px; fill: none; stroke: #efefef; stroke-width: 1px;}.choreo-bank-app-card__line-19-2 svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__line-19-2 svg, .choreo-bank-app-card__line-19-2 svg * { stroke: inherit !important; fill: inherit !important; stroke-width: inherit !important;}.choreo-bank-app-card__line-20 { position: absolute; left: 23px; top: 105px; width: 322px; height: 1px; fill: none; stroke: #efefef; stroke-width: 1px;}.choreo-bank-app-card__line-20 svg { display: block; width: 100%; height: 100%; overflow: visible;}.choreo-bank-app-card__line-20 svg, .choreo-bank-app-card__line-20 svg * { stroke: inherit !important; fill: inherit !important; stroke-width: inherit !important;}.choreo-bank-app-card__frame-1686561428 { position: relative; align-self: stretch; height: 51px; box-sizing: border-box; display: flex; gap: 10px; padding: 0px 24px; justify-content: space-between; align-items: center; background-color: #ffffff; overflow: hidden; border-radius: 20px;}.choreo-bank-app-card__direct-deposit-details { width: 163px; height: 19px; color: #b5b5b5; font-size: 16px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card__void-cheque { width: 72px; height: 15px; color: #b4b4b4; font-size: 12px; font-weight: 500; font-family: 'Inter', sans-serif; overflow-wrap: break-word;}.choreo-bank-app-card.is-bank-app-card__card-2-hover .choreo-bank-app-card__frame-1686561429 { transform: translateX(0px) translateY(16px); opacity: 0;}.choreo-bank-app-card.is-bank-app-card__card-2-hover .choreo-bank-app-card__card-2 { transform: translateX(0px) translateY(-28px) scaleX(1.55) scaleY(1.55);}.choreo-bank-app-card__switch.is-bank-app-card__switch-toggle { transform: translateX(0px) translateY(0px); background-color: #d8d8d8;}.choreo-bank-app-card__switch.is-bank-app-card__switch-toggle .choreo-bank-app-card__rectangle-9 { transform: translateX(-19px) translateY(0px);}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__card { transform: translateX(-9px) translateY(-9px) rotateY(0deg); border-radius: 23px; display: flex; height: 221px; opacity: 1; width: 386px;}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__card-2 { transform: translateX(2px) translateY(-23px) rotateY(-180deg) scaleX(1.7) scaleY(1.7); opacity: 0;}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__frame-1686561430 { transform: translateY(132px);}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__frame-1686561427-2 { transform: translateX(0px) translateY(0px); display: block; height: 106px; opacity: 1; width: 370px;}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__switch { transform: translateX(0px) translateY(0px);}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__your-virtual-card-will-have-a-different-number-than-your-physical-card-this-helps-keep-your-information-secure { display: block;}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__group { opacity: 0;}.choreo-bank-app-card[data-state="clicked"] .choreo-bank-app-card__direct-deposit { transform: translateX(0px) translateY(0px); opacity: 0.94;}@media (prefers-reduced-motion: no-preference) { .choreo-bank-app-card__frame-1686561429 { transition: transform 350ms cubic-bezier(0.25, 1, 0.5, 1), opacity 350ms cubic-bezier(0.25, 1, 0.5, 1); } .choreo-bank-app-card__card-2 { transition: transform 410ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 500ms cubic-bezier(0.33, 1, 0.68, 1); } .choreo-bank-app-card__switch { transition: transform 300ms cubic-bezier(0, 0, 0.58, 1), background-color 300ms cubic-bezier(0, 0, 0.58, 1); } .choreo-bank-app-card__rectangle-9 { transition: transform 300ms cubic-bezier(0, 0, 0.58, 1); } .choreo-bank-app-card__card { transition: transform 500ms cubic-bezier(0.33, 1, 0.68, 1), border-radius 500ms cubic-bezier(0.33, 1, 0.68, 1), height 500ms cubic-bezier(0.33, 1, 0.68, 1), opacity 500ms cubic-bezier(0.33, 1, 0.68, 1), width 500ms cubic-bezier(0.33, 1, 0.68, 1); } .choreo-bank-app-card__frame-1686561430 { transition: transform 500ms cubic-bezier(0.33, 1, 0.68, 1); } .choreo-bank-app-card__frame-1686561427-2 { transition: transform 500ms cubic-bezier(0.33, 1, 0.68, 1), height 500ms cubic-bezier(0.33, 1, 0.68, 1), opacity 500ms cubic-bezier(0.33, 1, 0.68, 1), width 500ms cubic-bezier(0.33, 1, 0.68, 1); } .choreo-bank-app-card__group { transition: opacity 500ms cubic-bezier(0.33, 1, 0.68, 1); } .choreo-bank-app-card__direct-deposit { transition: transform 500ms cubic-bezier(0.33, 1, 0.68, 1), opacity 500ms cubic-bezier(0.33, 1, 0.68, 1); } .choreo-bank-app-card__frame-1686561429 { will-change: opacity, transform; } .choreo-bank-app-card__card { will-change: opacity, transform; } .choreo-bank-app-card__card-2 { will-change: opacity, transform; } .choreo-bank-app-card__frame-1686561430 { will-change: transform; } .choreo-bank-app-card__frame-1686561427-2 { will-change: opacity, transform; } .choreo-bank-app-card__switch { will-change: transform; } .choreo-bank-app-card__rectangle-9 { will-change: transform; } .choreo-bank-app-card__group { will-change: opacity; } .choreo-bank-app-card__direct-deposit { will-change: opacity, transform; }}You never wrote it
No syntax, no coefficients, no class names to invent. All of it came from the layers you dragged.
Nothing to install
No Choreo runtime and no required library. Paste it into the project and it runs.
Reduced motion, handled
Every export honours a reduced-motion preference. You did not have to remember.
Or skip it entirely
Send a live link instead, and let the engineer lift these same files from the panel.
Then, at the very end, there's code
You never touched it while designing. These are the actual files Choreo emitted for the card you just used: 56 layers, three triggers and a state machine, in one pass.
Not just a timeline. A real editor.
Choreo opens your Figma frames but does not depend on them. Structure, style, states and motion are all editable in-tool.
Figma import that tells you the truth
The bundled plugin walks a selected frame in, with layout, vectors and sizing intact. Anything that could not translate exactly is listed in the Import Report and linked to the layer, so an imperfect import is a task instead of a silent lie. Re-import later and your choreography survives.
Named component states
Base plus named states, Figma-variant style. Wire a click, double-click or hover to a state, then read the whole machine as a graph.
cubic-bezier(
0.16, 1, 0.3, 1
)
Emphasized decelerate
Curves the browser can draw
A bezier editor with presets and springs that document their own fidelity. It cannot express a curve your export would have to fake.
Named as you keyframe it, not in an audit later.
Green until it isn't
A signal beside the timeline: green while the motion stays cheap, amber the moment it gets expensive, with the property named.
Share the interaction itself
Publish a link where stakeholders hover, click and scroll the real component. Engineers take the export from the same page, no account needed.
Local-first
Documents autosave to your browser and save out as portable files. Cloud sync is an optional layer you can switch on later.
Motion paths
Move a layer along a curve with editable tangents, not just from one point to another in a straight line.
Vector motion
Icons animate as real vectors: stroke draw-on, path morphing, gradient fills, with engine differences stated where they exist.
Presets and templates
Card float, jelly press, focus ring, toast slide-in. Drop one on and it stays fully editable, never a black box.
Why not just use Figma or Lottielab?
Because the three tools are different shapes. Figma owns structure, Lottielab owns illustrative motion, and neither is built to make a hover state behave in a browser.
| Choreo | Figma Motion | Lottielab | |
|---|---|---|---|
| Built for | Interface micro-interactions | Prototypes and design specs | Vector and illustrative animation |
| The canvas is | Real DOM, styled with real CSS | The Figma design canvas | A vector animation canvas |
| Time is organised by | One timeline per trigger state | Transitions between frames | A single linear timeline |
| Real UI states | Seven triggers, enter and exit edited separately | Prototype interactions on frames | Whatever the Lottie player exposes |
| What reaches production | CSS, Web Animations, React, or a written spec | An engineer rebuilds it by hand | A Lottie JSON file |
| Runtime needed to play it | None | None; it stays a prototype | The Lottie player |
| Handoff is | A live link stakeholders can interact with | A prototype link plus a written spec | A file plus the player that runs it |
Choreo is not trying to replace either one. It is for the layer in between, where a component has to respond to a real pointer. This comparison reflects how each tool positions itself today; all three keep moving.
Five commitments the tool holds
Each one is a constraint Choreo works within, and together they are the reason you can trust what comes out the other side.
- 01
Predictable, never guessed
Nothing is generated for you. Every result comes from choreography you performed, so the same file always behaves the same way.
- 02
The canvas is the browser
Your component is live in the page, so nothing sits between the canvas and what your users see. What you scrub is what ships.
- 03
States, not seconds
The web is event-driven, so the unit of choreography is a trigger state, not a position on a linear timeline.
- 04
Honest about limits
When an import cannot be translated faithfully, or something cannot animate smoothly, the tool tells you and points at the layer.
- 05
Deletable
What you hand off needs no Choreo runtime and no account. Drop the tool tomorrow and nothing it produced breaks.
Your next interaction starts here
Open a file, arm a hover, and drag something. Nothing to install, and nothing leaves your browser.
Free to use · no account · files stay local