Motion, Interaction & Prototyping
The craft that separates a DE from an FE engineer
Animation fundamentals & 60fps
After this you can build smooth animations that never drop frames.
Smooth motion is a budget problem. You have ~16.7ms per frame at 60fps and the difference between a Design Engineer and a front-end engineer is knowing exactly which CSS properties spend that budget cheaply.
Cursor runs in Electron, a Chromium renderer wrapped in a desktop shell. The browser does the same pipeline work it does on the web: style, layout, paint, composite. Animate the wrong property and you force the whole pipeline to re-run sixty times a second on a window that is already busy rendering a code editor.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The earlier a property change enters the pipeline, the more downstream work it forces every frame.
Animate on the compositor, not the main threadthe single most important rule
Two properties animate without touching layout or paint: transform and opacity. The compositor handles them on the GPU, often on a separate thread, so they keep moving even when JavaScript is blocked. Everything else is suspect.
- Property
- transform: translate / scale / rotate
- Pipeline cost
- Composite only
- Use for motion?
- Yes - the default tool
- Property
- opacity
- Pipeline cost
- Composite only
- Use for motion?
- Yes - fades, reveals
- Property
- top / left / margin
- Pipeline cost
- Layout → paint → composite
- Use for motion?
- No - causes reflow every frame
- Property
- width / height
- Pipeline cost
- Layout → paint → composite
- Use for motion?
- No - animate scale instead
- Property
- box-shadow / background
- Pipeline cost
- Paint → composite
- Use for motion?
- Avoid - paint cost adds up
- Property
- color / filter
- Pipeline cost
- Paint (filter can promote)
- Use for motion?
- Sparingly
| Property | Pipeline cost | Use for motion? |
|---|---|---|
| transform: translate / scale / rotate | Composite only | Yes - the default tool |
| opacity | Composite only | Yes - fades, reveals |
| top / left / margin | Layout → paint → composite | No - causes reflow every frame |
| width / height | Layout → paint → composite | No - animate scale instead |
| box-shadow / background | Paint → composite | Avoid - paint cost adds up |
| color / filter | Paint (filter can promote) | Sparingly |
If a property changes geometry, the browser must re-measure the page. Transform and opacity never do.
To slide a panel in, you almost never set left. You set transform: translateX(...). To grow a card, you do not animate width - you scale it and correct the children or you use FLIP. The visual result looks identical; the frame cost is not.
CSS animation vs the Web Animations APIdeclarative when you can, imperative when you must
CSS transitions and keyframes are the right default. They are declarative, the browser optimizes them and they survive a busy main thread. Reach for the Web Animations API when motion has to be dynamic - a value computed at runtime, a spring you cancel mid-flight or choreography you sequence in code.
Hover, focus, simple enter/exit
Known start and end states
Cheapest to write and to run
Hard to interrupt cleanly mid-flight
Runtime-computed values, sequencing
Cancel, reverse, read currentTime
Promise on .finished for chaining
Composites off-main-thread like CSS
const anim = el.animate(
[{ transform: "translateY(8px)", opacity: 0 },
{ transform: "translateY(0)", opacity: 1 }],
{ duration: 180, easing: "cubic-bezier(0.2, 0, 0, 1)", fill: "both" }
);
anim.finished.then(() => el.classList.add("settled"));
// Interruptible: anim.reverse() or anim.cancel() at any time.Springs vs easing curveswhy interactive motion feels alive
An easing curve has a fixed duration: it always takes 180ms regardless of how far the element travels. A spring is defined by physics - stiffness, damping, mass - so its duration emerges from the motion and it carries velocity. That is why a dragged panel that you release feels right with a spring and wrong with a tween.
- Easing for discrete state changes: a menu opens, a toast appears. Duration is known, distance is fixed.
- Spring for anything continuous or interruptible: drag-and-release, a value that can be re-targeted mid-flight, gestures that hand velocity to the animation.
- Settle on a small easing vocabulary. A standard ease-out for entrances, a sharper curve for exits and one spring config for interactive motion covers most of a product.
When asked to animate a panel resize, say it out loud: “I won’t animate width - that reflows every frame. I’ll FLIP it: measure before and after, then animate a transform from the delta.” Naming the cheap path before you write a line signals the exact instinct this role is hired for.
FLIP: animate layout changes cheaplyFirst, Last, Invert, Play
FLIP lets you animate a layout change using only compositor properties. You measure the element's First position, apply the Last layout instantly, compute the Invert transform that visually places it back at First, then Play by transitioning that transform to zero.
- 1First. Record the element’s box with
getBoundingClientRect()before the change. - 2Last. Make the DOM change (reorder, resize, reparent) and read the new box.
- 3Invert. Set a
transformthat maps Last back onto First, so it looks unmoved. - 4Play. On the next frame, transition the transform to
none- the browser animates the delta on the GPU.
Measuring instead of guessingthe performance panel is the source of truth
Open the Performance panel, record an interaction and look for long tasks and a green-bar frame rate that holds at 60. Purple layout bars firing every frame mean layout thrash: you are reading geometry and writing styles in the same loop, forcing synchronous reflow. Batch reads, then writes.
- Dropped frames
- Frames over ~16.7ms - the animation stutters
- Recalculate Style / Layout
- Per-frame purple bars = layout-triggering animation
- Forced reflow warning
- A read after a write in the same tick - batch them
- Long tasks (>50ms)
- Main thread blocked; compositor-only motion survives this
Perceived latency is not actual latency. A 100ms operation with an instant skeleton or optimistic state feels faster than a 60ms operation that shows nothing for 60ms. Measure both the frame rate and the time-to-first-feedback - users feel the second one more.
Takeaway. Animate transform and opacity on the compositor, use springs for interactive motion and easing for state changes, FLIP layout changes and verify in the Performance panel - never by eye.
Self-check
QYou need to animate a sidebar panel growing from 240px to 320px wide. What is the frame-cheap approach and why?
Micro-interactions & the 90→100 polish
After this you can identify and execute the details that make UI feel magical.
The JD says it plainly: get experiences from 90% to 100%. That last 10% is almost entirely micro-interactions and states - the work most engineers skip and most users feel.
A button that works is 90%. A button that has a focus-visible ring, a satisfying active depression, a hover transition tuned to ~120ms, a hit target that extends past its visible bounds and a disabled state that explains itself - that is the 100% Cursor is paying for.
Interaction states are not optionalhover, active, focus-visible, disabled
Hover hints affordance; active confirms the press
Tune transitions: ~120ms in, snappy out
On a desktop app, the cursor itself is feedback
Use :focus-visible, not :focus
Ring shows for keyboard, hides for mouse
Keyboard users are first-class in an editor
44px comfortable minimum, even when the glyph is small
Extend the clickable area past the visual with padding
An icon button that’s hard to hit reads as broken
pointer for actions, text for editable, grab for draggable
not-allowed on disabled, with a reason on hover
The wrong cursor quietly erodes trust
Motion that masks latencythe gap between click and result is where polish lives
An AI editor is full of operations that take real time: a completion arrives, a diff applies, an agent thinks. You cannot make the network faster, so you manage the perception of waiting.
- Optimistic UI for reversible actions: apply the change immediately, reconcile when the server confirms, roll back visibly if it fails.
- Skeletons over spinners when you know the shape of what’s coming - a skeleton that matches the final layout reads as “almost there” rather than “stuck.”
- Entrance choreography so content does not pop. A 150ms fade-and-rise on arriving results turns a jarring snap into something that feels intentional.
- Honest progress for long operations: a determinate bar when you have real progress, a calm indeterminate state when you don’t. Never fake a percentage.
The states where magical is won or lostempty, loading, error, first-run
Engineers build the happy path and ship. The states around it are where craft shows, because they are the moments a user is most likely confused or frustrated.
- State
- Empty
- Lazy version
- Blank panel
- 100% version
- Explains what goes here and the one action to fill it
- State
- Loading
- Lazy version
- Centered spinner
- 100% version
- Skeleton matching final layout; optimistic where safe
- State
- Error
- Lazy version
- “Something went wrong”
- 100% version
- What failed, why and a retry that actually retries
- State
- First-run
- Lazy version
- Drop user into a cold UI
- 100% version
- A first action that produces a visible win in seconds
| State | Lazy version | 100% version |
|---|---|---|
| Empty | Blank panel | Explains what goes here and the one action to fill it |
| Loading | Centered spinner | Skeleton matching final layout; optimistic where safe |
| Error | “Something went wrong” | What failed, why and a retry that actually retries |
| First-run | Drop user into a cold UI | A first action that produces a visible win in seconds |
Each of these is a chance to make the product feel considered or careless.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The same feature, shipped two ways - the gap is the craft this role is hired for.
Timing, choreography & reduced motionstagger with intent and honor the preference
When several elements enter at once, a small stagger - 20 to 40ms between items - reads as orchestrated instead of chaotic. Keep durations short: 150 to 250ms for most UI motion. Long animations feel slow on the tenth use even if they delight on the first.
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* Better: keep opacity fades, drop large translates/parallax for
users who get motion sick. Reduced motion != no feedback. */prefers-reduced-motion: reduce does not mean remove all feedback. It means cut vestibular triggers - big translations, parallax, spin. Keep opacity fades and instant state changes so the UI still confirms the user’s action.
Critique drill: find five improvementsthe skill the portfolio round tests
Take a real Cursor interaction - say, accepting an inline completion - and force yourself to list five concrete polish improvements. Not vibes, specifics you could ship.
- Tune the ghost-text fade so accepted text settles in ~80ms instead of snapping.
- Add a
focus-visiblering to the accept affordance so keyboard users see the target. - Stagger multi-line diff reveals by ~25ms per line so the change reads top to bottom.
- Give the reject path the same motion weight as accept, so dismissing never feels punished.
- Show a calm pending state while the model streams, not a spinner that implies a hang.
“The interaction worked, but it snapped. I added an 80ms ease-out on the accepted text and a 25ms stagger across diff lines so the change reads in order. Small numbers, but it’s the difference between functional and considered.”
Takeaway. The 90→90 to 100 jump is interaction states (hover/active/focus-visible/disabled), latency-masking motion and the empty/loading/error/first-run states most engineers skip - always with a reduced-motion path.
Self-check
QA teammate sets prefers-reduced-motion: reduce to strip every animation and transition from the app. What’s the problem and what’s the better approach?
Rapid prototyping
After this you can turn an idea into an evaluable artifact fast.
Prototyping is written into the role: rapidly prototype concepts to validate interaction and product ideas before committing to full builds. The skill is not building fast - it is building the right amount before you throw it away.
On an AI-native editor, interaction paradigms are still being invented. You cannot spec your way to whether a new diff UI feels good; you have to make it clickable and put hands on it. The 2-day in-person trial is, in large part, a test of exactly this loop.
Have a stack you can stand up in minutesthe cost of starting must be near zero
If spinning up a prototype takes an hour, you will prototype less and learn slower. Practice until a blank canvas is a two-minute command, not a project.
- Scaffold
- Vite + React or Solid -
npm create vite@latest, running in seconds - Styling
- Plain CSS or a utility layer you already know cold; skip setup ceremony
- Motion
- One animation lib you trust or hand-rolled WAAPI for control
- Escape hatch
- A browser sandbox when local setup isn’t worth it for a tiny idea
- Fake data
- Hardcoded fixtures and setTimeout to simulate latency and streaming
The exact tools matter less than your fluency with them under a clock.
Fidelity disciplinematch fidelity to the question
Every prototype answers a question. Build only enough to answer it. Over-building is the most common prototyping mistake, because polishing feels like progress while it quietly burns the time you needed to test the idea.
- The question
- Does this motion feel good?
- Right fidelity
- One screen, real timing, fake data
- What to skip
- Routing, state, real backend
- The question
- Is this flow understandable?
- Right fidelity
- Clickable happy path, low visual polish
- What to skip
- Edge cases, error states, theming
- The question
- Will this scale to 10k rows?
- Right fidelity
- Throwaway perf harness, real data volume
- What to skip
- Visual design entirely
- The question
- Does the team buy the direction?
- Right fidelity
- Polished hero moment only
- What to skip
- Everything outside the demo path
| The question | Right fidelity | What to skip |
|---|---|---|
| Does this motion feel good? | One screen, real timing, fake data | Routing, state, real backend |
| Is this flow understandable? | Clickable happy path, low visual polish | Edge cases, error states, theming |
| Will this scale to 10k rows? | Throwaway perf harness, real data volume | Visual design entirely |
| Does the team buy the direction? | Polished hero moment only | Everything outside the demo path |
A prototype that answers its question and nothing else is a good prototype.
A prototype is an argument, not a foundation. Build it so you are willing to delete it. The moment you start protecting prototype code, you have stopped prototyping and started maintaining - and you will defend a worse idea because you are attached to the work.
Prototype-to-production judgmentwhat to keep, what to rewrite
Sometimes a prototype earns a path to production. Knowing what survives that transition is its own craft. The interaction model and the timing usually keep; the implementation usually does not.
The validated interaction model and choreography
Tuned timing/easing values you proved by feel
The component API shape that felt right to use
Hardcoded data, faked latency, shortcut state
Accessibility and edge cases you deferred
Anything that skipped the design system
Practice timed prototype sprintsrehearse the rhythm of the trial
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The rhythm of the 2-day trial, compressed - most of the clock goes to the hero, not setup.
- 1Pick a question (5 min). State the single thing the prototype must answer. Write it down.
- 2Scaffold (5 min). Blank Vite app running, fixtures in place, one screen rendering.
- 3Build the hero (45 min). The interaction and motion that answer the question, nothing else.
- 4Critique (10 min). Use it for real, list five polish gaps, fix the top two.
- 5Present the decision (5 min). What you learned and what you’d build next - not a feature tour.
In the trial, narrate fidelity choices as you go: “I’m faking the stream with setTimeout because the question is whether the reveal feels right, not whether the network works.” It shows you scope deliberately, which is the judgment they’re grading as much as the output.
Takeaway. Keep a stack you can stand up in two minutes, build only enough fidelity to answer one question and treat prototype code as a deletable argument - the interaction model survives, the implementation usually doesn’t.
Self-check
QYou’re prototyping to find out whether a new inline-diff reveal animation feels good. What fidelity is appropriate and what should you deliberately skip?
AI-native interaction paradigms
After this you can reason about novel UI patterns specific to AI dev tools.
Cursor’s edge as a product is its interaction language for AI: inline diffs, tab prediction, agent surfaces. These patterns are young, which is precisely why a Design Engineer who can reason about them is valuable.
The throughline across all of them is trust and control. An AI is acting on the user’s code and the UI’s job is to keep the human in command - every AI action must be legible, reversible and dismissable without friction.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Ranked by how fast their absence makes a user stop inviting the AI in.
Inline diffs and accept/rejectmake AI edits legible and reversible
Show exactly what changed, in place, not in a side panel the eye must reconcile
Added vs removed must be instantly scannable
Large diffs need structure: collapse, jump-to-next-hunk
Accept and reject at the right granularity: hunk, file, all
Reject must be one keystroke and leave zero residue
Nothing the AI does should be hard to undo
Reject deserves as much design care as accept. If dismissing an AI suggestion is slow or leaves the buffer altered, users stop trusting the feature and stop inviting the AI in at all.
Tab / ghost-text predictionlatency, dismissal and trust
Ghost text is a high-frequency interaction - it fires on nearly every keystroke - so its rules are unforgiving. It must be visually distinct from real code, it must never block typing and it must vanish the instant the user types something else.
- Distinct
- Dimmed/styled so it never reads as committed code
- Non-blocking
- Typing always wins; the prediction never fights the cursor
- Latency-tolerant
- Appears when ready, never makes the user wait to type
- Instant dismissal
- Keep typing and it’s gone - no modal, no confirm
- Calibrated
- Wrong suggestions erode trust fast; restraint beats volume
Agent and chat UIsstreaming, tool calls, interruption, context
An agent runs over time and takes actions, which is a harder UI problem than a chat bubble. The user needs to follow what it’s doing, trust the steps and stop it the moment it goes wrong.
- Streaming so the user sees thinking arrive token by token - silence reads as a hang, even when work is happening.
- Tool-call surfaces that make each action visible: which file it read, what command it ran, what it’s about to change.
- Interruption as a first-class control. A stop button that actually halts the run, immediately, is non-negotiable for trust.
- Context display so the user knows what the agent can see. Hidden context is the fastest way to lose a user’s confidence.
Keyboard-first and command surfacesthe primary interaction model for power users
Developers live on the keyboard. A command palette is often the main entry point to functionality, not a fallback. Design it as a primary surface: fast fuzzy matching, discoverable shortcuts shown inline and a flow that never forces a reach for the mouse.
Every AI-native pattern reduces to one question: does the user stay in control? Legible diffs, instant dismissal, visible tool calls, a real stop button. When you design these, you are not decorating AI - you are keeping a human in command of their own code.
Asked how you’d improve an AI editing flow, anchor on trust and control rather than aesthetics: “My first move is making reject as fast and clean as accept, because the failure mode for AI features is users not trusting them enough to invite them in.” That framing shows you understand what makes these products work.
Takeaway. Every AI-native pattern - inline diffs, ghost text, agent surfaces, command palettes - lives or dies on trust and control: legible, reversible, dismissable, interruptible, with visible context.
Self-check
QYou’re asked how you’d improve Cursor’s inline-diff accept/reject flow. What principle should anchor your answer?
Figma-to-code fidelity
After this you can translate designs faithfully and push back with technical insight.
The bridge between design and engineering runs both directions. You translate Figma into pixel-perfect code and when implementation reveals a flaw the design tool couldn’t show, you propose the fix.
Faithful translation starts with reading Figma like a system, not a picture. Auto-layout maps to flexbox and gap; constraints describe responsive behavior; variants are your component’s state matrix; tokens are the values you should never hardcode.
Reading a Figma file structurallyauto-layout, constraints, variants, tokens
- Figma concept
- Auto-layout
- Code equivalent
- flex / grid + gap
- What to extract
- Direction, gap, padding, alignment, hug vs fill
- Figma concept
- Constraints
- Code equivalent
- Responsive rules
- What to extract
- What pins, what stretches as the container resizes
- Figma concept
- Variants
- Code equivalent
- Component prop matrix
- What to extract
- Every state/size combination the API must express
- Figma concept
- Tokens / styles
- Code equivalent
- CSS variables
- What to extract
- Color, type, spacing, radius - never magic numbers
- Figma concept
- Component instances
- Code equivalent
- Reused components
- What to extract
- Where the design system already has a primitive
| Figma concept | Code equivalent | What to extract |
|---|---|---|
| Auto-layout | flex / grid + gap | Direction, gap, padding, alignment, hug vs fill |
| Constraints | Responsive rules | What pins, what stretches as the container resizes |
| Variants | Component prop matrix | Every state/size combination the API must express |
| Tokens / styles | CSS variables | Color, type, spacing, radius - never magic numbers |
| Component instances | Reused components | Where the design system already has a primitive |
Read the structure first; the pixels follow from it.
Match exactly, then improve where code canfidelity first, judgment second
Get spacing, type and color exact before you get clever. Pull the real values, don’t eyeball them. Then apply the things a static design tool literally cannot express, because that is where code adds value the mockup couldn’t.
- Real interaction states - hover, focus-visible, active, disabled - that the frame only hints at.
- Motion and transitions between the states a designer drew as separate frames.
- Fluid behavior across viewport and density that fixed-width artboards can’t represent.
- Edge cases: long strings, empty data, RTL and the dark-mode pairing of every token.
Overlay your build on the Figma frame at 100% and diff it. Check spacing with the measure tool, sample colors with a picker, confirm font metrics. “Looks right” is not pixel-perfect; an 8px gap rendered at 6px is the kind of miss this role is hired to catch.
The bidirectional bridgewhen code reveals a design flaw, propose the fix
Implementation surfaces problems Figma hides: a layout that breaks at a real string length, a contrast ratio that fails in dark mode, a transition the static frames implied but never specified. The Design Engineer doesn’t silently “fix” it or rigidly ship the broken spec - you raise it with a concrete proposal.
“The header works in the mock, but at a real 40-character title it wraps and pushes the action off-screen. I’d truncate with a tooltip and keep the action pinned - here’s a quick build of both. Which reads better to you?”
Tokens as the single source of truthone definition, two consumers
When Figma and code drift, every screen becomes a negotiation. Keep design tokens authoritative - color, spacing, type, radius defined once and consumed by both the design tool and the codebase, ideally via an export pipeline so a token change propagates instead of being hand-copied.
- 1Define once. Tokens live in a single source - a tokens file or a Figma variables set that exports.
- 2Consume in both. Figma styles and CSS variables both reference the same names.
- 3Change in one place. Update the token; Figma and code both move, no per-screen edits.
- 4Guard drift. A renamed or removed token should break loudly, not silently mismatch.
Be ready to walk through a real translationthe portfolio round will ask
Have one Figma-to-code story rehearsed in detail: the frame you started from, the values you matched, the flaw implementation exposed, the fix you proposed and the polish you added that the mock couldn’t show. That single story demonstrates the whole bridge at once.
When you walk a translation, lead with a decision, not a feature list: “The spec used a 200ms linear fade; in the build it felt sluggish, so I moved to a 150ms ease-out and showed both to the designer.” Defending a craft decision with a reason is exactly what the portfolio round grades.
Takeaway. Read Figma structurally (auto-layout, constraints, variants, tokens), match values exactly then add what code can do that the mock can’t and when implementation exposes a flaw, propose the fix - with tokens as the shared source of truth.
Self-check
QWhile building a header from a Figma frame, you discover that a realistic 40-character title wraps and pushes the primary action off-screen - the mock only showed a short title. What’s the right move?