Precise visual edits

For agents

Agents write the code now. But we’re still typing “the blue button, make it less round” and waiting to see whether the result matches what we meant. Hope isn’t a spec.

If I can see what needs to change, I should be able to change it. Move it, adjust the spacing, try another type size. Make the decision on the page, with everything around it.

That’s why I’m building designmode. It puts a canvas on your running app. Select anything and change what you see. Position, layout, type, colour. Then give your agent the exact diff: component, file and line, before and after.

Your source stays untouched. Every edit is a live override. The export is the deliverable. No screenshots to annotate. No translating a decision into prose so it can be interpreted again.

Design and code need a shared way to communicate intent clearly and directly. The edit is the message.

CHAPTER 01Getting started

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.

FIG. 001ANATOMY OF A VISUAL EDIT
01 / Select & editA temporary overlay02 / Your appReal DOM. Live preview.03 / Change listPricingCard.tsx:24ONE ELEMENT. THREE CONNECTED VIEWS.
001 / Your app is the canvas. The overlay measures and previews the edit; the export tells a person or agent exactly what to change.

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

AreaWhat it covers
EditingSelect, move, resize, rotate, group, insert, delete. Position, auto layout, sizing, typography, states. The right click menu.
Fills, images and masksSolid, gradient and image fills on boxes and on text. Native image replacement. Alpha and luminance layer masks.
Named stylesTypography, border, shadow and gradient styles read from your tokens and classes. Apply, override, detach.
Responsive previewPhone, tablet and custom widths in a real frame. Native media queries, not a CSS imitation.
CommentsPins and regions with images and @mentioned layers, addressed to source locations.
Design tokensYour own tokens by name, with the file that defines them. Utility classes treated as tokens.
AgentsSend 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.

CHAPTER 02Getting started

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.

FIG. 002FIVE MOVES TO A FIRST HANDOFF
01Shift+DOpenThe bar appears02clickSelectHandles and panel03gap 20EditLive on the page04CCommentPin the intent05CopyHand offPaste to your agentABOUT FIVE MINUTES, INCLUDING THE INSTALL.
002 / Open, select, edit, comment, hand off. The whole tutorial is these five moves. Everything else in the manual is depth on one of them.
  1. 1 min

    Install

    Add one line to your dev index.html and reload. Any React 18+ app in a dev build works. Or npm i -D designmode and import 'designmode/auto' in your entry file, if you'd rather it lived in the repo.

    <script src="https://unpkg.com/designmode"></script>
  2. 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.

  3. 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 do

    Select 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
  4. 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 do

    Pin a comment on the Get started button.

    The export says
    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.
  5. 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-5 and "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.
CHAPTER 03Getting started

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 designmode
import '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 }).

Dev server only. Not on Vite? Hit the same HTTP endpoints yourself, or just keep using the clipboard.

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-mcp

Once 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 doctor

doctor 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.

CHAPTER 04Using designmode

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.

FIG. 003ONE PROPERTY, ONE VISIBLE CHANGE
PricingCard / selected
01Make room
for ideas.
12
02A little
more space.
LIVE PREVIEW / INLINE OVERRIDE
AUTO LAYOUT
CHANGE LISTgap: 12px → 12px
003 / Try the spacing control. The preview and change list describe the same edit. In designmode, Copy exports it; Discard restores the original.

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.
You do

Select three cards, press Shift+A, then set the new group's gap to 24.

The export says
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: 24px

The 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+50 evaluates.
  • ↑ / ↓ 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.

You do

Switch the state to Hover and darken the button's fill.

The export says
<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 → #000

Pause 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.

CHAPTER 05Using designmode

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.

FIG. 004ONE LAYER REVEALS ANOTHER
img.herodiv.blobTwo siblings, selected togetherUse as maskalpha maskor luminance⌃⌘M toggles itSource hides its own paint
004 / The bottom sibling becomes the mask source. Its shape reveals the layers above it. Both stay in the DOM and stay editable; the export describes the relationship, not a flattened image.

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.
You do

Select the blob and the photo, press ⌃⌘M.

The export says
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.
CHAPTER 06Using designmodesince 0.11

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.

FIG. 005A STYLE IS ONE NAME, NOT SIX FIELDS
TYPOGRAPHYInterRegular3441-0.40LeftSix raw valuesapplyTYPOGRAPHYLarge Title / Regular · 34/41⤫Letter spacing -0.2Modifiedtokens/type.json:8One name. One override. Detach with ⤫.
005 / Apply a named style and the raw controls it owns collapse into its name. Edit a field and it reads Modified. Detach and the fields come back with the values they had.

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.

You do

Apply Large Title to the heading, then tighten letter spacing.

The export says
<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.2px
You do

Detach the card's shadow style and keep what's on screen.

The export says
<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 values
CHAPTER 07Using designmodesince 0.10

Responsive 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.

FIG. 006THE SAME APP AT THREE WIDTHS
Desktop · full widthTablet · 810Phone · 390grid-template-columns: 321
006 / With the Vite bridge the app runs inside a real resizable frame. Pick a width and the browser's own media queries, matchMedia and viewport units answer, not a CSS imitation.

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.

CHAPTER 08Using designmode

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.

FIG. 007FEEDBACK WITH AN ADDRESS
Get started1“Make this feel heavier.”Hero → button.ctasrc/sections/Hero.tsx:9:5Intent + exact location
007 / A comment stays attached to an element. Its component, selector and source location travel with the words.

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.png
CHAPTER 09Using designmode

Design 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.

FIG. 008A NAME, A VALUE, A SOURCE
tokens.css:12--space-lg: 24px;Authored in your projectRESOLVED IN THE BROWSER24px
008 / The source scan finds the token’s name and definition. Runtime resolution finds the value your element actually uses.

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.

You do

Click the gap chip and pick --space-5 instead of --space-3.

The export says
<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 verdict

Only missed is designmode's fault. Without the Vite plugin you only get the runtime half. Which tokens exist comes from the source scan.

CHAPTER 10Using designmode

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, ::after and ::marker get 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 with pointer-events: none and display: contents wrappers don't hide what's under them.
  • SVGs are one layer, like an image. The tree doesn't descend into them.
CHAPTER 11Agents

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.

FIG. 009CLOSING THE LOOP
01EditYour browser02RecordLocal inbox03ApplyYour agent04VerifyLive pageReview the result. Keep iterating.
009 / Send records the change in the inbox. A watching agent claims it, edits the source, then resolves it. The page can be measured again to verify the result.

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-mcp

The connection is a sentence

Say this in the terminal you want doing the work:

"Watch designmode and apply my design changes as they arrive."

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

  1. You edit, then hit Send to agent. The change list is recorded first. Send writes .designmode/requests/<id>.md before 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.
  2. The watching agent returns from designmode_watch with the items and works through them, calling designmode_resolve per item. The overlay tracks it live: a toast shows "2/3 done" and surfaces the agent's questions (designmode_reply).
  3. The agent's word isn't proof. designmode_resolve only 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

StateMeaningThe fix it names
Bridge offDisabled in settingsFlip the switch
No bridgeThe browser cannot reach /__designmode/*Add the Vite plugin
No agentNo MCP server registeredThe mcp add command, one click to copy
Not watchingAgent registered, nobody parkedSay the sentence in a terminal
WatchingGreen, named sessionSend 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 doctor

The 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.

CHAPTER 12Agents

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
ToolWhat it does
designmode_pendingList the design changes handed off from the overlay
designmode_watchBlock until new changes arrive, then return them
designmode_resolveMark one change as applied to the source
designmode_dismissDecline one change with a reason the user sees
designmode_replyAsk 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.

timeoutSecondsnumber

How 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").

idstringrequired

The item id from designmode_pending / designmode_watch.

notestring

One line on what changed (file, prop, value).

designmode_dismiss

Declines one change with a reason the user will see.

idstringrequired

The item to decline.

reasonstring

Why 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.

idstringrequired

The item the question is about.

textstringrequired

The 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.

CHAPTER 13Agents

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)
LineWhat it pins down
Element + owner stackThe tag as rendered, the component that wrote the JSX, and the components above it, each with its source location
file:line:colThe JSX source location, from the React fiber (dev builds). Kept as the path your dev server reports
SelectorA CSS path to the element, for when source attribution isn't available
ClassesThe element's own classes, when it's styled by utilities, so the agent finds the right JSX
DiffsOne 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>.md in 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.

CHAPTER 14Agents

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.

EndpointRole
/pingBridge liveness. The overlay checks this first
/statusThe status ladder: bridge, inbox, agent, watchers
/tokensDesign tokens and named styles scanned from source, with file:line
/handoffThe overlay posts a Send here. Items are recorded to the inbox
/pendingAgent pulls unclaimed items
/watchLong poll: blocks until new items or timeout. This is the connection
/claimAtomic claim: one request goes to exactly one session
/resolveMark an item applied
/dismissDecline an item with a reason
/replySurface a question in the overlay
/imageComment and fill image upload, saved to .designmode/
/ackThe callback proof. An agent shows it can act
/agent-hello · /agent-byeMCP server lifecycle
/probeVerify support: measure again after an agent reports done
/text-sourceResolve text content back to its source location
/inboxList the request files on disk
/viewport.jsThe responsive shell script the Vite plugin injects before your app
These are dev server routes, same origin with your app. Nothing designmode does is reachable from production.
CHAPTER 15Reference

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

ActionKeys
Open / minimize the barShiftD
Also open / minimize (where the browser allows)⌘E
Minimize from insideM
BrowseB
SelectVD
CommentC
TextT
Rectangle / ellipse / lineROL
Unwind: popover → tool → selection → barEsc

Selection

ActionKeys
Toggle a layer in the selection⌘click
Marquee select⌘drag
Select the current level (siblings)⌘A
Canvas: drill into first child / select parentEnterShiftEnter
Tree: select parent / first child↑↓
Right click menu for the selectionShiftF10
Edit text in placedoubleclick

Editing

ActionKeys
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 layoutShiftA
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

ActionKeys
Step a numeric field (bigger / finer)↑↓Shift⌥
Next / previous valueTabShiftTab
Cycle a field’s token, detach and sizing buttons⌥↓
Move through a dropdown / jump by typing↑↓a–z

Zoom

ActionKeys
Zoom about the cursor⌘wheel
Zoom in / out+-
Back to 100%0
Pan while zoomedSpacedrag

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.

CHAPTER 16Reference

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:fixed host 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 a requestAnimationFrame loop. 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.

CHAPTER 17Reference

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 except file: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/viewport entry. 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 200px to 300px in 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.
CHAPTER 18Reference

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/viewport entry 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, ::after and list markers as selectable layers.
  • Elements inside dialogs and popovers, under pointer-events: none overlays, and in display: contents wrappers 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.