The designmode manual.
Your running React app is a canvas. Select something, change what you see, and give your agent an exact change list. This manual explains how the pieces fit together.
Press Shift+D on your dev build (or ⌘E where the browser lets you), click anything, and edit its position, auto layout, typography, fills, strokes or effects. Apply your project's own named styles and tokens. Preview it at phone and tablet widths. Hit Copy or Send to agent to hand the changes to Claude Code, Codex, Cursor, or a teammate.
It never touches your source files. Every edit is a live inline style override on the page. The export is the deliverable.
Why it exists
I built this because handoff is broken. A designer can see exactly what's wrong with a shipped UI and still can't say it in a way that survives the trip to the codebase. "Make the gap bigger and the card less round" turns into a Slack thread, a screenshot and a guess. designmode turns direct manipulation into a diff nobody has to interpret:
<div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9) in Pricing (at src/sections/Pricing.tsx:40:7)
selector: section.pricing > div.card
- border-radius: 16px → 10px
- gap: 12px → 20px
On page /, <button class="cta">Get started</button> in Hero (at src/sections/Hero.tsx:9:5)
(header.hero > button.cta) has this comment: "should feel heavier" — make the change it describes.An agent (or a person) applies that against the real codebase, using whatever styling approach the project already has. Every change names the component that wrote the JSX, the components above it, their source file:line, a selector, and the before and after values. Nothing to interpret, nothing to guess.
What's in the box
| Area | What it covers |
|---|---|
| Editing | Select, move, resize, rotate, group, insert, delete. Position, auto layout, sizing, typography, states. The right click menu. |
| Fills, images and masks | Solid, gradient and image fills on boxes and on text. Native image replacement. Alpha and luminance layer masks. |
| Named styles | Typography, border, shadow and gradient styles read from your tokens and classes. Apply, override, detach. |
| Responsive preview | Phone, tablet and custom widths in a real frame. Native media queries, not a CSS imitation. |
| Comments | Pins and regions with images and @mentioned layers, addressed to source locations. |
| Design tokens | Your own tokens by name, with the file that defines them. Utility classes treated as tokens. |
| Agents | Send to a watching agent over MCP, track progress live, verify the result on the page. |
Requirements
- React 18+ running a development build. Source locations are dev only. See Limitations.
- Desktop browser. The tool is keyboard and pointer driven.
The 30 second path
<script src="https://unpkg.com/designmode"></script>Drop that in your dev index.html, reload, press Shift+D. That's it. The agent loop, token reading from source, real responsive frames and comment images are all optional and layer on top. If you have five minutes, do Your first handoff next. It ends with a real export in your clipboard.
Your first handoff
Five minutes, one script tag, and you finish with a change list an agent can apply. No Vite plugin, no MCP. Those come later and make it faster, not possible.
- 1 min
Install
Add one line to your dev
index.htmland reload. Any React 18+ app in a dev build works. Ornpm i -D designmodeandimport 'designmode/auto'in your entry file, if you'd rather it lived in the repo.<script src="https://unpkg.com/designmode"></script> - 10 sec
Open it
Press Shift+D. A white pill appears at the bottom of the page and a panel docks on the right. Your app is still your app: press B for Browse and clicks go through to it. Press V to come back to Select.
- 1 min
Change one thing
Click a card, a button, anything with a bit of padding. The panel shows its real values. Change gap or border radius in the panel, or drag a handle. The page updates live. Nothing is written to your files. Try ⌘Z and watch it come back.
You doSelect the pricing card and set Radius to 10.
The export says<div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9) selector: section.pricing > div.card - border-radius: 16px → 10px - 1 min
Say the thing you can't click
Some feedback has no CSS yet. Press C, click the primary button, and type "should feel heavier". The pin is addressed to the component that rendered the button, not to a spot on the screen.
You doPin a comment on the Get started button.
The export saysOn page /, <button class="cta">Get started</button> in Hero (at src/sections/Hero.tsx:9:5) (header.hero > button.cta) has this comment: "should feel heavier" — make the change it describes. - 30 sec
Hand it off
Press V to leave the comment tool, then hit Copy above the bar. The page hands itself back: the live edits clear, and the change list lands in your clipboard and in History (three dots menu) in case you want it again. Paste it into Claude Code, Codex, Cursor, or a pull request. This is what they get:
<div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9) selector: section.pricing > div.card - border-radius: 16px → 10px On page /, <button class="cta">Get started</button> in Hero (at src/sections/Hero.tsx:9:5) (header.hero > button.cta) has this comment: "should feel heavier" — make the change it describes.
Where to next
- Make Copy into Send. Add the Vite plugin and register the MCP server, say one sentence in a terminal, and the agent pulls changes itself. Installation, then The agent loop. About ten minutes.
- Use your own tokens and styles. The same plugin scans your source, so the panel offers
--space-5and "Large Title" instead of raw numbers. Design tokens and Named styles. - Check it at phone width. Hover the pill at the top of the page. Responsive preview.
Installation
One script tag is the whole install. npm, the Vite bridge and the MCP server are optional. Each one adds one thing the page can't do on its own.
Script tag
<script src="https://unpkg.com/designmode"></script>Drop it in your dev index.html and press Shift+D. The overlay starts on load. ⌘E / Ctrl+E also works unless your browser reserves it, which is why Shift+D exists. The pill at the bottom of the page opens it too.
npm
npm i -D designmodeimport 'designmode/auto' // initializes on load
// or, to control the timing:
import { init } from 'designmode'
init()Only import it in development. It's a dev tool, and source locations don't exist in production builds anyway.
Vite bridge
The bridge does the things a page can't do for itself. It runs your app inside a real resizable frame for responsive preview, receives handoffs for agents, scans your project for design tokens and named styles, and writes images attached to comments and fills into .designmode/ at the project root, so the export can name a file your agent can actually open.
// vite.config.ts
import designmode from 'designmode/vite'
export default defineConfig({
plugins: [react(), designmode()],
})If your app has to run at the top level of the window, keep the bridge and drop the frame with designmode({ viewport: false }).
Responsive preview without Vite
The frame has to exist before the app starts, so this is a separate browser entry, not a plugin:
import { createViewport } from 'designmode/viewport'
if (!createViewport()) {
await import('./app') // mounts your app and initializes designmode
}The outer page becomes the editor shell. The same URL loads inside an iframe and mounts the app once. Keep app startup inside the dynamic import. This can't be bolted onto an app that's already running. The Vite plugin does all of this for you.
MCP server
Register the MCP server with your agent and the loop closes:
# Claude Code
claude mcp add designmode -- npx -y --package=designmode designmode-mcp
# Codex
codex mcp add designmode -- npx -y --package=designmode designmode-mcpOnce a session is parked in the watch loop, the primary button becomes Send to agent. A bridge on its own isn't enough. The button only promises delivery when someone is actually waiting. The full flow, including the one sentence that connects a terminal, is in The agent loop.
Verify the setup
npx designmode-mcp doctordoctor runs the same five checks as the overlay's MCP row (three dots, then MCP), from the terminal: bridge reachable, inbox writable, agent registered, watcher parked, round trip proven. Exit 0 is healthy, 2 is working but nobody's watching, 1 is broken. The round trip is proved by doing it. It writes a real request and waits for the callback. It's not a config audit.
Editing
Select elements on the canvas, drag their handles, and adjust properties in the design panel. Every edit is a revertible inline style override, and even Discard is one ⌘Z away from coming back.
The bar
One white pill at the bottom of the page. One row, no mode switcher. Left to right: Browse, Select (wearing the logo) and Comment, then a three dots menu with everything that isn't a pointer (the inserts, Pause and Speed, the MCP row, History, the keyboard shortcut reference), and a Minimize X at the end. When you have changes, the Send to agent / Copy button and Discard sit above the row. The tool you leave is the tool you come back to. A full reload or an MPA link restores it.
Browse (B) lets clicks reach the page, so links, buttons and forms work while your edits stay live. Select (V or D) is where you edit. Comment (C) pins an element or a region. The inserts, Text (T), Rectangle (R), Ellipse (O) and Line (L), draw a new layer that exports as "Added …". Minimize (M) tucks the bar into an edge tab that reopens on click, never on hover. Every key is in one place: Keyboard shortcuts.
Design mode is never really "off". Shift+D toggles between the tab and the bar, and Esc unwinds one layer at a time: open popover, then tool, then selection, then the bar itself. Selection and edits survive a trip through Browse, so you can click through a few routes and carry on.
Selection and movement
- Select. Click. ⌘+click toggles a layer in or out of the selection, ⌘+drag marquees. ⌘A takes the current level (the siblings of what's selected), never the whole page. Layers under inert overlays and inside dialogs and popovers are reachable.
- Drill. Enter selects the first child, Shift+Enter the parent. In the layers tree it's ↓ and ↑.
- Move. Drag. Flex children reorder live, in their visual direction even in RTL rows. ⌥+drag forces a free move.
- Resize / rotate. Handles. The zones just outside the corners rotate. Shift snaps to 15°. Outlines follow a rotated layer's real corners.
- Flip. Shift+H horizontal, Shift+V vertical.
- Nudge. Arrow keys (Shift = 10px). Inside an auto layout parent the main axis arrow reorders among siblings. ⌥+arrow forces a plain nudge.
- Copy / cut / paste / duplicate. ⌘C, ⌘X, ⌘V, ⌘D on layers.
- Group. Shift+A wraps the selection in an auto layout container. ⌘⇧G ungroups it, and also unwraps containers your page already had, keeping their children and loose text in place.
- Hide and lock. ⌘⇧H hides (or shows) a layer, ⌘⇧L locks it against selection. Both are also in the right click menu.
- Edit text. Double click a text layer to edit in place. Picking any tool ends the edit.
- Delete. Delete or Backspace. Exports as "Remove …". Discard restores.
- Undo / redo. ⌘Z / ⌘⇧Z (Ctrl+Y off Mac), with focus anywhere except inside a text field.
Select three cards, press Shift+A, then set the new group's gap to 24.
Wrap the following elements (in this order) in a new flex container:
- <div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9)
- <div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9)
- <div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9)
Container styles: flex-direction: row; gap: 24pxThe right click menusince 0.11
Right click a layer on the canvas or in the layers tree. The menu carries every layer action with its shortcut beside it: Copy, Paste, Cut, Duplicate, Select parent / child, Add comment, Add auto layout, Ungroup, Use as mask (with Alpha / Luminance once one exists), Hide, Lock, Flip horizontal / vertical, Delete. Right click a different layer and the menu moves there. Shift+F10 opens it for the current selection from the keyboard.
Zoom
Canvas zoom preserves the layout at the current viewport size. The page scales, the tool doesn't, nothing reflows. What you're inspecting is the layout this viewport produces, just bigger. ⌘+wheel (or a trackpad pinch) zooms around the cursor. + and - step, 0 goes back to 100%, hold Space and drag to pan. 100% is the floor. There's no artboard to zoom out of. While zoomed, a pill on the bar shows the percentage and doubles as the button back to 100%.
The panel
The panel groups properties into sections: Position, Auto layout, Appearance, Typography, Fill, Stroke, Effects. The inputs support:
- Drag to scrub on labels.
- Math in any numeric field.
300+50evaluates. - ↑ / ↓ step a numeric field. Shift for bigger steps, ⌥ for finer ones where the property supports it.
- Tab / Shift+Tab move between values and skip the token and sizing buttons embedded in a field. ⌥+↓ cycles through those buttons when you want them.
- Dropdowns take arrow keys, and typing jumps to a matching option. Font family is searchable.
- "Mixed" on multi select.
- Fixed / Hug / Fill per axis. If the layout can't deliver a mode (Hug on a box with nothing to measure, Fill under a parent with no size on that axis), it's greyed out with the reason. It's never written.
- Min and max width and height on freeform layers, without turning on auto layout.
- Auto gap distributes children (
space-between). While it's on, the alignment pad becomes a cross axis picker.
Element states and pseudo elementssince 0.9
The state switcher at the top of the panel edits :hover, :focus, :active and :disabled. The overlay forces the state on while you edit, so you actually see what you're changing. The same switcher lists a layer's existing ::before, ::after and ::marker, and the layers tree shows them as rows, so a decorative pseudo element or a list bullet is editable like any other layer.
Switch the state to Hover and darken the button's fill.
<button class="cta">Get started</button> in Hero (at src/sections/Hero.tsx:9:5)
selector: header.hero > button.cta
- [on :hover] background-color: #111 → #000Pause and Speed
Both live in the three dots menu. Pause freezes the page's motion: CSS animations, transitions, Web Animations, video, gifs, SMIL, and JS driven motion (Lottie, GSAP, framer motion, canvas tickers). It works on all of them because it stops the page's clock, not just its animations. Speed runs that same clock at 0.25× to 4×. Both are view states, not edits. They never show up in the export, and releasing Pause only resumes the motion it stopped.
Reset and Discard
Reset reverts one element's changes in a single step and leaves the rest alone. Discard drops everything, and is itself one ⌘Z from coming back. Copy and Send also clear the live preview, but they file the edits in History first, so nothing is lost.
Fills, images and masks
A layer has a list of fills, not one colour. Boxes, lines and shapes paint their box. Text layers paint their glyphs. Images are a path the agent can open, and a sibling layer can be a mask.
The fill list
Each fill is solid, gradient or image, with its own opacity and remove button. + adds another. One picker edits whichever row you click: type icons, the ramp with draggable stops right above the colour square, hue, alpha, channel cells, format and eyedropper, then past a hairline the stops as a list plus the angle. Select a text layer and the same controls paint the letters instead of the box, gradients and images included.
- A gradient is edited where it's painted. While its picker is open, handles appear on the element itself. Drag a square to move a stop, the knob to rotate.
- Named gradients from your tokens and classes attach as a style, per fill layer, without disturbing the other paints.
Image fills
Choosing Image paints a checkerboard placeholder straight away. Upload a file, or drop one onto the preview or the swatch. A replacement keeps the sizing and crop settings of the image it replaces. A file the browser can't decode leaves the current image alone and says so.
- Four scale modes: Fill, Fit, Crop, Tile. Each one is just the CSS that expresses it.
- Crop directly on the canvas. Crop adds a Scale cell (the picture's width as a % of the box) next to X and Y, and choosing it keeps the picture exactly where Fill had it. While the picker is open the canvas ghosts the whole picture around the frame, and dragging the element pans it. One undo step. Discard restores.
- An image fill is a path, not a payload. With the Vite bridge the bytes go to
.designmode/and the export names the file.
Native imagessince 0.11
A real <img> uses the same picker to swap its source and choose Fill or Fit. It keeps its browser layout. Undo or Discard puts the original back, including the responsive srcset and <picture> sources, and the export tells the agent to update those too.
Layer maskssince 0.11
Select two or more sibling layers and pick Use as mask from the right click menu, or press ⌃⌘M (Ctrl+Alt+M off Mac). The bottom layer becomes the mask source. Its shapes, text, images and transparency reveal the selected content above it, and its own paint is hidden. Right click the source to switch between Alpha and Luminance.
- The layers keep their DOM positions and their responsive layout, and each one stays individually editable. Move the source and the reveal moves with it.
- Undo, Discard and History restore the relationship, not a flattened picture.
- The export names the source and the masked layers and asks for CSS or SVG masking that keeps them independent. It never ships the preview as an image.
- Not supported: a source with a perspective transform, groups over 500 elements, and images or fonts the browser can't read cross origin.
Select the blob and the photo, press ⌃⌘M.
Use <div class="blob">…</div> (section.hero > div.blob) as an alpha mask for:
- <img class="cover"> (section.hero > img.cover)
Keep the mask and content independently editable. Preserve their responsive layout and relative alignment; hide the mask source's own paint.Named styles
A design system doesn't think in six font properties. It thinks in "Large Title". Named styles let the panel do the same, using the styles your project already defines.
Apply, replace, modify, detach
Open the four circle styles icon in a section header, beside + where there is one. Applying a style shows its name once, in place of the raw controls it owns. Properties the style doesn't cover, and any local overrides you already had, stay visible. Click the chip to replace the style. Click the button beside it to detach, which exposes the individual fields with the values the style was giving them. Edit one field under a style and it reads Modified. Replacing or detaching is one undo step. Empty sections stay compact.
Typography
One picker holds text styles and font family tokens. A full text style bundles the related font properties and summarises itself as size over line height: Large Title / Regular · 34/41. A font family token changes only the face and leaves size, weight and spacing alone. Detach to search or type a raw family.
Styles come from three places in your source:
- Compound text classes, where one class sets several font properties.
- Explicit typography objects in token JSON, in the common DTCG shape, local aliases included.
- Token families that share a path and name their font properties.
Equal values alone never attach or merge a style. Two families that happen to resolve the same stay two styles. Colour and layout are independent of typography. CSS variables, relative units, unitless line heights and the original class or token names all survive into the handoff.
Borders, shadows and gradients
The same choose, replace, modify and detach controls sit on the Stroke, Effects and Fill sections. They read named CSS variables, the relevant declarations from classes, and composite tokens in JSON. A complete width / style / colour token family forms a border style on its own.
- A border style keeps the sides that were active. Detaching it keeps its separate colour and width tokens; detach those individually for raw values.
- A shadow style preserves a separate focus ring.
- A gradient style lives in its own fill layer, so the other paints and any image positioning are untouched. A JSON gradient with only colour stops keeps the page's direction and shape.
Colour roles
Colour tokens are grouped by the role your project gives them: Text, Surface, Border, Accent, Icons, and named palettes. Each token keeps its identity and opacity. Two tokens that render the same colour but carry different roles stay separate, because a theme can change them independently and the agent needs the right name.
In the export
The style is named with where it comes from (a token, a class, or a token family) and its defining file. Overrides follow as ordinary diffs. A detached style says so, and asks the agent to keep the current values.
Apply Large Title to the heading, then tighten letter spacing.
<h1 class="title">…</h1> in Hero (at src/sections/Hero.tsx:14:7)
selector: header.hero > h1.title
- typography style: family text.title (tokens/type.json:8); apply only this style's typography properties, with the per-property overrides below
- letter-spacing: -0.4px → -0.2pxDetach the card's shadow style and keep what's on screen.
<div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9)
selector: section.pricing > div.card
- detach shadow style token shadow.card (tokens/effects.json:21); keep the current valuesResponsive preview
Pick a width and the app relays out for real. Media queries, matchMedia, viewport units, resize events and fixed positioning all answer to the frame, because the app is running inside one.
The width bar
A quiet pill at the top centre of the page. Hover it for three chips, Desktop (full width), Tablet (810px) and Phone (390px), plus a field for any width from 320px up. ↑ / ↓ step the field. Widths wider than the canvas scroll sideways. The width is a view state like Pause: it never appears in the export, and Browse or minimizing releases it.
How the frame works
With the Vite plugin, the dev server serves a thin shell at your app's URL. The shell hosts the editor, loads the same URL once more inside an iframe, and mounts your app there, once. Resizing the frame is a real browser resize. App state and the editor's undo history survive it. The panel and the bar live outside the frame, so they never eat into the width you're previewing.
Not on Vite? Use the designmode/viewport entry described under Installation. It has to run before the app mounts. With a bare script tag and neither shell, width chips fall back to a CSS only approximation: media queries are simulated, but JavaScript viewport reads and viewport units don't change.
Opting out
designmode({ viewport: false })Some apps have to be the top window: they read window.parent, they set frame blocking headers, or they're already inside another frame. Pass this and the bridge keeps working while the app runs at the top level. Frame blocking policies are always respected.
Comments
Some feedback isn't a property change. "This should feel heavier" has no CSS diff yet. Comments carry that intent into the same export, pinned to real source locations.
Pins and regions
With the comment tool (C), a click pins an element and a drag annotates a region, even empty space. Add comment in the right click menu pins the layer under the cursor. A pinned element exports with its component name, file:line and selector. So the comment is addressed to a place in the codebase, not a place on the screen.
Images
Paste, drop or pick an image in the comment composer. "Make it look like this." With the Vite bridge the image is saved under .designmode/ at the project root and exported as a path, so the agent can open the file you meant.
@mention layers
Mention other elements inline with the composer's reference button (the cursor add icon). Arm it, then click a layer on the page or in the tree. Each mention resolves to component + file:line in the export. "Align this with @Sidebar" arrives with both ends named.
What the agent sees
On page /, <button class="cta">Get started</button> in Hero (at src/sections/Hero.tsx:9:5)
(header.hero > button.cta) has this comment: "should feel heavier" — make the change it describes.
Attached image: .designmode/img-8f2c.pngDesign tokens
The panel offers your project's own tokens, read from your source, with the name you gave them and the file:line that defines them. Not a generic palette.
Two halves, merged
- Source scan (via the Vite bridge). CSS custom properties, Sass/Less variables, and token JSON, each with its authored name and defining
file:line. Composite tokens become named styles. - Runtime resolution. The value actually in effect on the element, resolved through the cascade. Handles oklch palettes, shadcn channel triples,
clamp()/calc(). This is what draws the swatch.
Tokens are grouped by role and searchable by name or value. Source wins. A custom property that only exists on the rendered page isn't offered when the scan returned anything, because an agent can't grep for it.
Attached, replaceable, detachable
When a design token governs a property, the field shows the name, not the value. Click the chip to replace it. Click the button beside it to detach and go back to a raw value. Both live in the field, on every field alike: colour, spacing, radius, type. Tab skips those buttons and ⌥+↓ reaches them.
Click the gap chip and pick --space-5 instead of --space-3.
<div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9)
selector: section.pricing > div.card
- gap: 12px → 20px (replace token --space-3 with --space-5)Token or plumbing?
There's no list of framework names. A variable is told apart from plumbing by how the page declares it:
| Declared… | Verdict |
|---|---|
| where it reaches the root (a theme block, :root, html) | Token. Offered |
| only on a component selector (.btn { --btn-h }) | Scoped. Offered, resolved against an element the rule reaches |
| with @property inherits: false (Tailwind v4, Mantine, MUI internals) | Plumbing. Withheld |
| on the universal selector (Tailwind v3 defaults) | Plumbing. Withheld |
| once, and consumed by the same rule (.bg-white { --tw-bg-opacity; … var(--tw-bg-opacity) }) | Plumbing. Withheld |
The known cost: a hand written component variable declared once and used only by its own rule gets withheld too. One that's overridden anywhere, or consumed by a child rule, is a real API and survives. I think that's the right trade.
Utility classes
In a Tailwind style codebase the class is the token. The field shows bg-bg-secondary, clicking it offers the other classes your project ships, and picking one swaps the class in place so cascade order survives. Detach removes the class and pins every value it was providing. Each field is independent: detach the colour class, keep the font it also set. Utilities inside @layer (Tailwind v4) are indexed like any other rule. Only conditional rules (@media, @supports, @container) count as variants.
Auditing a miss
Which token is in effect is read from the cascade, so same origin stylesheets only. When a token you expect doesn't show up:
__DESIGN_MODE__.audit(el) // per property: literal | internal-only | undetermined | missed
__DESIGN_MODE__.origin('--tw-shadow') // 'root' | 'scoped' | 'plumbing' — the classifier's verdict
__DESIGN_MODE__.pageTokens() // every custom property the page declares, with its verdictOnly missed is designmode's fault. Without the Vite plugin you only get the runtime half. Which tokens exist comes from the source scan.
Layers
A tree of the page on the left, with component chips at React fiber boundaries. It's the DOM you're editing, labelled with the components that own it.
Component chips
bippy maps DOM elements to React fibers. The owner stack names the component that wrote the JSX and the components above it, with their source locations. Those names show up as chips in the tree, and they're the same names the export uses.
Working the tree
- Double click a layer to rename it. The name carries into exports.
- Selection works both ways. Pick in the tree or on the canvas. ⌘+click toggles, ↑ selects the parent, ↓ the first child, and a canvas selection scrolls its row into view.
- Right click a row for the same menu the canvas has: copy, paste, duplicate, comment, group, ungroup, mask, hide, lock, flip, delete.
- Grouping (Shift+A) wraps the selection in an auto layout container that shows up in the tree like any other layer. Ungroup works on your page's own containers too.
- A bare text run can be promoted to a layer of its own from the tree, so a single word inside a paragraph can carry a style.
- A layer's
::before,::afterand::markerget rows of their own when they render, so bullets and decorative pseudo elements are selectable. - Portalled content (Radix dialogs, dropdowns, toasts mounted on
body) gets rows as top level siblings of the app root. Overlays withpointer-events: noneanddisplay: contentswrappers don't hide what's under them. - SVGs are one layer, like an image. The tree doesn't descend into them.
The agent loop
Instead of pasting the prompt yourself, let your agent pull it. The design splits two jobs with opposite failure profiles. A file never fails to record and never wakes anyone. A live connection wakes instantly and fails constantly. So the file is the truth and the wake is best effort, and nothing you experience depends on the fragile half.
Setup
// vite.config.ts
import designmode from 'designmode/vite'
export default defineConfig({
plugins: [react(), designmode()],
})# Claude Code
claude mcp add designmode -- npx -y --package=designmode designmode-mcp
# Codex
codex mcp add designmode -- npx -y --package=designmode designmode-mcpThe connection is a sentence
Say this in the terminal you want doing the work:
That session parks in designmode_watch, and the parked long poll is the connection. Not a heartbeat inferring liveness, not a config file implying it. A session holding a line open means delivery is immediate. The status dot and the Send button follow parked watchers, never the MCP heartbeat. A process being alive says nothing about anyone waiting for work.
The loop
- You edit, then hit Send to agent. The change list is recorded first. Send writes
.designmode/requests/<id>.mdbefore anything else, so the send has genuinely succeeded the moment the file exists. The icon beside Send still copies the same text for a teammate or another tool. - The watching agent returns from
designmode_watchwith the items and works through them, callingdesignmode_resolveper item. The overlay tracks it live: a toast shows "2/3 done" and surfaces the agent's questions (designmode_reply). - The agent's word isn't proof.
designmode_resolveonly flips an entry to resolved. Applied is earned by measurement. The overlay measures again every unverified entry against the live page (every few seconds while active, and immediately once everything is resolved) and only flips it when every measurable style has moved off its recorded original. An agent that resolved without changing anything gets caught here instead of silently reverting your work. Still look at the page. A resolved item proves the agent changed something, not that it changed it well.
Several sessions watching? A send goes to exactly one (an atomic claim), and the MCP row tells you which.
History
A handoff hands the page back. The live overrides are cleared (undoably) and the edits live on as a version history entry: the prompt, which layers and properties it touched, and per style snapshots. History is kept per tab, so it survives the reloads HMR causes and doesn't outlive the session. Capped at 30. Three dots, then History lists them with a status each: sent, resolved, applied. Every entry can copy its prompt again. An unapplied entry offers Restore, which reapplies the snapshots as live overrides. An applied one offers a revert prompt telling the agent to put each original back, because only the agent can undo edits to source.
Status: five states, one fix each
| State | Meaning | The fix it names |
|---|---|---|
| Bridge off | Disabled in settings | Flip the switch |
| No bridge | The browser cannot reach /__designmode/* | Add the Vite plugin |
| No agent | No MCP server registered | The mcp add command, one click to copy |
| Not watching | Agent registered, nobody parked | Say the sentence in a terminal |
| Watching | Green, named session | Send delivers straight to it |
The MCP row in the three dots menu shows the dot, the state in a word, and a copy icon for whichever fix applies. The dot also rides the three dots button as a badge. Dead loops go dark. No rearm within one watch window and the watcher expires. Ctrl+C, context ran out, or the model decided it was done all look the same from here, and the fix is the same sentence again. Resolving the last item releases the claim, so a finished session never looks connected for the rest of its lease. The connection has an off switch in the same row. Off means no request goes out at all, not just "it's down".
Honest toasts
- Picked up, working. An agent really pulled it. This is the one place the spinner earns its keep.
- Copied. The quiet toast. Nobody's watching, so nothing was promised. The file is on disk and the prompt is on your clipboard. No spinner, and it dismisses itself. The handoff is still tracked, so it upgrades to the live loop the moment an agent pulls it.
Never "stalled", because nothing failed. Every failure toast carries its fix. Watcher died: say the sentence again. Nothing pulled with a watcher parked: check the watching terminal. Dev server gone: your changes are safe in history. The live toast's failure states offer Restore directly. Otherwise Restore lives in History.
Diagnosis from the terminal
npx designmode-mcp doctorThe row's ladder as a CLI. Exit 0 healthy, 2 working but no watcher, 1 broken. It proves the round trip by doing it, and it never announces itself as an agent. A diagnosis that makes "agent connected" self fulfilling isn't a diagnosis.
MCP tools
The MCP server ships as a bin in the npm package (designmode-mcp) and talks to the Vite bridge over HTTP. Five tools. That's the agent's whole vocabulary.
# Claude Code
claude mcp add designmode -- npx -y --package=designmode designmode-mcp
# Codex
codex mcp add designmode -- npx -y --package=designmode designmode-mcp| Tool | What it does |
|---|---|
designmode_pending | List the design changes handed off from the overlay |
designmode_watch | Block until new changes arrive, then return them |
designmode_resolve | Mark one change as applied to the source |
designmode_dismiss | Decline one change with a reason the user sees |
designmode_reply | Ask the user a question, shown live in their overlay |
designmode_pending
Lists the design changes the user handed off from the overlay. Each item has an id, the page (route) it was made on, a CSS selector, the owning React component with source file:line when available, and a before and after description of the change. Apply each one to the source, then mark it with designmode_resolve. No parameters.
designmode_watch
Blocks until the user hands off new changes (or the timeout passes), then returns them. This is the live loop: watch, apply, resolve, watch again. While an agent waits here, the user sees it as connected. Their Send button is live and delivers straight to that session. Leaving the loop turns it off.
timeoutSecondsnumberHow long to wait before giving up. Default 55, max 240. On timeout the tool returns a prompt to call it again. The agent rearms and the user never sees a gap.
designmode_resolve
Marks one change as applied to the source. Call it after each item so the user sees progress live ("2/3 done").
idstringrequiredThe item id from designmode_pending / designmode_watch.
notestringOne line on what changed (file, prop, value).
designmode_dismiss
Declines one change with a reason the user will see.
idstringrequiredThe item to decline.
reasonstringWhy it wasn't applied. Wrong element, conflicts with the design system, that kind of thing.
designmode_reply
Asks the user a question about one change, shown live in their overlay. "Should this apply to every card or just this one?" The user answers in the agent's chat.
idstringrequiredThe item the question is about.
textstringrequiredThe question or status message to show the user.
Proof, not configuration
Configuration isn't evidence. Every request file ends with a callback line the agent runs:
curl -s "http://localhost:<port>/__designmode/ack?token=<token>"The agent proves itself, which means verification keeps working for agents that ship after designmode does. The token is 16 hex characters naming a request the bridge wrote. Anything else is a 404 and changes nothing. Acking the current request ends the work (resolves its items, releases the claim). Acking an older one, or the doctor's probe, is proof of pickup only. Proof persists in .designmode/state.json and decays into "last seen X ago" instead of latching green forever.
Export format
The export is the deliverable. A change list precise enough that an agent, or a person, can apply it against the real codebase with nothing to interpret.
Anatomy of an item
<div class="card">…</div> in PricingCard (at src/components/PricingCard.tsx:24:9) in Pricing (at src/sections/Pricing.tsx:40:7) in App (at src/App.tsx:12:5)
selector: section.pricing > div.card
classes: card rounded-2xl gap-3
- border-radius: 16px → 10px — in classes: rounded-2xl → rounded-[10px]
- gap: 12px → 20px (the token --space-5)| Line | What it pins down |
|---|---|
| Element + owner stack | The tag as rendered, the component that wrote the JSX, and the components above it, each with its source location |
file:line:col | The JSX source location, from the React fiber (dev builds). Kept as the path your dev server reports |
| Selector | A CSS path to the element, for when source attribution isn't available |
| Classes | The element's own classes, when it's styled by utilities, so the agent finds the right JSX |
| Diffs | One property per line, before and after, in resolved CSS values. A token is named in brackets, and a utility class swap follows the diff, when one applies |
Styles, images and masks
- shadow style: token shadow.card (tokens/effects.json:21); apply only this style's shadow properties, with the per-property overrides below
- image src: /img/hero.jpg → .designmode/img-3a91.jpg
Update the responsive image sources (srcset / picture) to use the replacement too.
Use <div class="blob">…</div> (section.hero > div.blob) as an alpha mask for:
- <img class="cover"> (section.hero > img.cover)
Keep the mask and content independently editable. Preserve their responsive layout and relative alignment; hide the mask source's own paint.A named style is one line that says which style, where it's defined, and that only its own properties should change. Image fills and native image swaps export as paths under .designmode/. A mask is a relationship between named layers, never a flattened picture.
Comments
Comments ride along in the same list, addressed the same way:
On page /, <button class="cta">Get started</button> in Hero (at src/sections/Hero.tsx:9:5)
(header.hero > button.cta) has this comment: "should feel heavier" — make the change it describes.Image attachments export as paths under .designmode/. @mentioned layers resolve to component + file:line.
Everything exported
If an agent can't see it, the edit doesn't exist. So every kind of edit is in the list, addressed the same way: style diffs, :hover/:focus/:active/:disabled rules, pseudo element edits, utility class swaps (pl-10 → pl-6), token attach / replace / detach, named styles, moves and reorders, groups and ungroups, flips, inserts ("Added …"), deletes ("Remove …"), hidden layers, text edits (including whether the words were a literal, a prop or an expression at the JSX call site), layer renames, image fills and swaps, masks, and comments. Pause, Speed, Zoom and the preview width are view states and never appear.
Where it goes
- Copy. The clipboard, always. It works 1000% of the time, so it's never hidden. With a bridge but no watcher it also leaves the request in
.designmode/requests/for later pickup. - Send to agent. Writes
.designmode/requests/<id>.mdin the project root first, then wakes the watching session. The clipboard becomes a view of that file. The file is reviewable, greppable, diffable, and readable by every agent that exists or ever will.
The agent is expected to apply changes using the project's existing styling approach. A Tailwind project gets class edits, a CSS modules project gets CSS edits. The diff says what changed. The codebase decides how.
HTTP endpoints
The MCP server is a thin client of the Vite bridge. If you're not on Vite, implement or proxy the same HTTP endpoints and you keep the whole loop. Every route lives under /__designmode/ on your dev server's origin.
| Endpoint | Role |
|---|---|
/ping | Bridge liveness. The overlay checks this first |
/status | The status ladder: bridge, inbox, agent, watchers |
/tokens | Design tokens and named styles scanned from source, with file:line |
/handoff | The overlay posts a Send here. Items are recorded to the inbox |
/pending | Agent pulls unclaimed items |
/watch | Long poll: blocks until new items or timeout. This is the connection |
/claim | Atomic claim: one request goes to exactly one session |
/resolve | Mark an item applied |
/dismiss | Decline an item with a reason |
/reply | Surface a question in the overlay |
/image | Comment and fill image upload, saved to .designmode/ |
/ack | The callback proof. An agent shows it can act |
/agent-hello · /agent-bye | MCP server lifecycle |
/probe | Verify support: measure again after an agent reports done |
/text-source | Resolve text content back to its source location |
/inbox | List the request files on disk |
/viewport.js | The responsive shell script the Vite plugin injects before your app |
Keyboard shortcuts
Everything on the keyboard, in one place. Off Mac, ⌘ is Ctrl and ⌥ is Alt. The tool's own labels follow the platform, and the same reference lives under three dots, then Keyboard shortcuts.
Tools
| Action | Keys |
|---|---|
| Open / minimize the bar | ShiftD |
| Also open / minimize (where the browser allows) | ⌘E |
| Minimize from inside | M |
| Browse | B |
| Select | VD |
| Comment | C |
| Text | T |
| Rectangle / ellipse / line | ROL |
| Unwind: popover → tool → selection → bar | Esc |
Selection
| Action | Keys |
|---|---|
| Toggle a layer in the selection | ⌘click |
| Marquee select | ⌘drag |
| Select the current level (siblings) | ⌘A |
| Canvas: drill into first child / select parent | EnterShiftEnter |
| Tree: select parent / first child | ↑↓ |
| Right click menu for the selection | ShiftF10 |
| Edit text in place | doubleclick |
Editing
| Action | Keys |
|---|---|
| Free move (ignore flex reorder) | ⌥drag |
| Rotate snap 15° | Shiftdrag |
| Nudge 1px / 10px (reorders inside auto layout) | ↑↓←→Shift |
| Force a plain nudge inside auto layout | ⌥↑↓←→ |
| Copy / cut / paste layers | ⌘CXV |
| Duplicate in place | ⌘D |
| Group in auto layout | ShiftA |
| Ungroup | ⌘⇧G |
| Use as mask / remove mask | ⌃⌘M |
| Hide / show | ⌘⇧H |
| Lock / unlock | ⌘⇧L |
| Flip horizontal / vertical | ⇧H⇧V |
| Delete element | ⌫ |
| Undo / redo | ⌘Z⇧Z |
Panel fields
| Action | Keys |
|---|---|
| Step a numeric field (bigger / finer) | ↑↓Shift⌥ |
| Next / previous value | TabShiftTab |
| Cycle a field’s token, detach and sizing buttons | ⌥↓ |
| Move through a dropdown / jump by typing | ↑↓a–z |
Zoom
| Action | Keys |
|---|---|
| Zoom about the cursor | ⌘wheel |
| Zoom in / out | +- |
| Back to 100% | 0 |
| Pan while zoomed | Spacedrag |
Zoom keys are unmodified on purpose. ⌘+ / ⌘- / ⌘0 are browser page zoom and a page can't cancel them. Keys designmode doesn't bind (⌘R, ⌘F, ⌘P) stay the browser's.
How it works
One invariant everything else follows from: the tool draws on top of someone else's app, so it has to be impossible for the two to interfere.
Source attribution
bippy maps DOM elements to React fibers. The owner stack names the component that wrote the JSX and the components above it. On React 18 the location comes from fiber._debugSource. React 19 removed it, so designmode reads debug stacks and resolves them through source maps asynchronously, which yields real files under dev servers like Vite. The export keeps the path the dev server reports.
Isolation
- The entire UI (canvas overlay + panel) lives in a shadow DOM on a single
position:fixedhost appended to<html>, so it can't collide with your app's styles. - With the responsive shell, your app and the canvas overlay run inside the frame while the panel lives in the parent document. Both share one editor store. Without a shell they share one document.
- The panel is Preact + htm, not React. It must not interfere with the host app's React tree, and htm means the library doesn't need JSX compilation.
- Selection handles and guides are drawn on a
<canvas>in arequestAnimationFrameloop. A transparent interaction layer captures pointer events only while design mode is active.
Edits
Edits are recorded per element as { prop, original, value } overrides in a central store, applied as inline styles, fully revertible via Discard. Named styles, masks, image swaps and structural moves are recorded beside them with enough to restore the original, which is what History's Restore replays. Nothing writes to your files. The export is the only output.
Zoom, pause and width
Zoom is a transform: scale() on <body>, so the page magnifies and nothing reflows. The host div hangs off <html> and stays at 100%. Pause wraps requestAnimationFrame and performance.now so JS driven motion parks along with the CSS kind. The tool's own loops run on the native clock, which is why the bar stays clickable while the page is frozen. The preview width resizes the frame the app lives in, so the browser does the responsive work itself. All three are view states and hand the page back at exit.
Dev bridge security
One gate in front of every /__designmode/* route: the peer must be loopback, and browser cross site requests are refused. That closes both the LAN exposure of vite --host and the CSRF vector. /text-source requires a source extension under the project root, so it can't be used to learn whether an arbitrary path exists. Only a named session registers as a watcher.
Limitations
Stated up front, because each one is the consequence of a deliberate choice, not an accident.
- Dev builds only for source locations. React 18 provides
fiber._debugSource. React 19 uses debug stacks and asynchronous source map resolution, so missing maps or framework debug data can limit attribution. Server Components haven't been verified against a real Next.js app yet. On production builds everything still works exceptfile:line. - Edits are inline style overrides. They don't survive a reload and they don't author media queries. That's the point. The export is the deliverable, not the page state. History keeps the prompts, per tab.
- Real responsive preview needs a shell. The Vite plugin or the
designmode/viewportentry. A bare script tag gets a CSS only approximation that can't change JavaScript viewport reads or viewport units. - Token reading is same origin. Which token is in effect is read from the cascade, so CORS applies to stylesheets. Which tokens exist comes from the source scan and isn't affected.
__DESIGN_MODE__.audit(el)names the reason for any miss. - Structural edits move React owned DOM. Reorders, groups and inserts are fine for preview and export. A re render of that subtree may conflict. Discard restores order.
- Geometry is approximate at the edges. Pseudo element bounds are estimated. Perspective transforms and transformed modal dialogs can misplace handles. A mask source can't have a perspective transform, and masks stop at 500 elements.
- A same size HMR edit can leave the CSS indexes stale. The stylesheet cache keys on rule count, so editing
200pxto300pxin a rule without adding or removing one isn't noticed until the next reload. - Zoom can't go below 100%. There's no artboard. The page is laid out to the viewport, so zooming out would just shrink it into a corner.
- Desktop browsers. Keyboard and pointer are the primary workflow. Narrow previews get compact controls, but touch only editing hasn't been verified.
Changelog
What each release added, newest first. Pages and sections in this manual carry a since 0.11 mark when they describe something recent. The full log lives with the code.
0.11.0
15 September 2026. Named styles, responsive previews and keyboard editing.
- Typography, border, shadow and gradient styles from token families, composite JSON tokens and CSS classes, with per field overrides, Modified markers, replace, detach and undo.
- Colour libraries grouped by role without merging distinct tokens that resolve to the same colour.
- Text fills: solid, gradient and image paint on glyphs. Image replacement by upload or drop, crop settings kept, responsive image sources restored on undo.
- Alpha and luminance layer masks from sibling layers, with an export that describes the relationship.
- The
designmode/viewportentry for native responsive previews outside Vite, and compact inspector controls at narrow widths. - Shift+D as a browser safe toggle, right click layer menus, V/B/C/T/R/O/L tool keys, tree navigation with ↑/↓, Tab between field values, font family search, and selection through inert overlays.
0.10.0
8 September 2026. Native responsive canvas and reliable edit recovery.
- With the Vite plugin the app runs inside a real resizable frame, so CSS and JavaScript breakpoints, viewport units and positioned elements follow the chosen width.
- App state and undo history survive resizing. Editor controls live outside the frame.
- Discard and undo restore group membership, sibling order, wrapper edits, image attributes and style priority correctly.
- A Chromium, Firefox and WebKit regression matrix.
0.9.0
7 September 2026. More layers, precise sizing, clearer handoffs.
- Min and max width and height on freeform layers.
::before,::afterand list markers as selectable layers.- Elements inside dialogs and popovers, under
pointer-events: noneoverlays, and indisplay: contentswrappers are reachable. - Rotated outlines follow real corners. RTL flex rows reorder in their visual direction.
- The export carries the component owner stack with source paths, resolved asynchronously through source maps on React 19.
- ↑/↓ step numeric fields, with Shift and Alt for larger and finer steps.
0.8.0
7 September 2026. Direct image cropping. One bar, one row.
- Crop is a real image fill mode, edited on the canvas by dragging the element. A Scale cell beside X and Y.
- The mode switcher is gone. One pill: Browse, Select, Comment, a three dots menu, Minimize. The tool persists across navigations.
- Fixed, Hug and Fill read the authored cascade, not computed style.
Earlier releases, from the first script tag to the agent loop, are in the changelog on GitHub.