---
title: Design Mode
description: Connect the visual preview editor with the browser SDK or submit headless design edits
docs_index: /docs/llms.txt
lastUpdated: 2026-10-23
product: v0 API
type: guide
related:
  - /docs/api/v2/reference/messages/send-message
  - /docs/api/v2/reference/messages/send-message-streaming
  - /docs/api/v2/reference/messages/send-message-async
  - /docs/api/v2/guides/accessing-previews
  - /docs/api/v2/guides/resuming-streams
  - /docs/api/v2/guides/custom-chat-interface
---

> For an index of all documentation, see [/docs/llms.txt](/docs/llms.txt).

Design Mode applies visual edits to your application's source code. There are two ways to integrate it:

- **Visual editing:** use `createDesignModeBridge` from `v0/browser` to connect your frontend to the editor runtime already included in v0-proxied previews. Your backend saves the edits through API v2.
- **Headless editing:** send structured `designMode` input to an existing chat without a browser or embedded editor.

The browser helpers have no React dependency and do not export an editor component. They connect to the existing preview runtime; your application owns authentication, chat access, and API calls.

Design Mode uses a model to translate rendered element descriptions and requested changes into source edits. It is not a deterministic DOM patch API: selectors and previous values help the model locate the source, but are not checked preconditions. Provide selectors, text, and component names that match the current version of your application.

## Connect the visual editor

Use a `v0` SDK release that exports `createDesignModeBridge` from `v0/browser` and `parseDesignModeMessage` from `v0`. Keep `V0_API_KEY` exclusively on your backend.

### Prepare the preview iframe

Follow [Accessing Previews](/docs/api/v2/guides/accessing-previews) first: serve the preview through a proxy on an isolated origin, configure trusted preview hosts, and forward document, asset, and navigation requests with the preview access token. A raw sandbox URL or deployed application does not necessarily contain the Design Mode runtime.

The iframe must already be in the DOM. If you use the iframe `sandbox` attribute, permit scripts and preserve the iframe's origin; do not place untrusted previews on your application's origin. `targetOrigin` is the exact origin of the iframe, including your own preview proxy when one is used, not the upstream sandbox URL or a wildcard.

### Create the browser bridge

```typescript title="design-mode.ts"
import { createDesignModeBridge } from 'v0/browser'

const iframe = document.querySelector<HTMLIFrameElement>('#preview')
if (!iframe) throw new Error('Preview iframe not found')

const chatId = 'chat_abc123'
const bridge = createDesignModeBridge({
  iframe,
  targetOrigin: new URL(iframe.src, window.location.href).origin,
  enabled: true,
  async onApply(message, { signal }) {
    // Forward to your authenticated backend, never directly to api.v0.dev.
    const response = await fetch(
      `/api/chats/${encodeURIComponent(chatId)}/design-mode`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(message),
        signal,
      },
    )
    if (!response.ok) throw new Error('Unable to save design edits')
    return true
  },
  onError(error) {
    console.error('Design Mode:', error.message)
  },
})

await bridge.ready
```

The helper can connect before or after the runtime loads. `ready` resolves after the runtime handshake checks the iframe window and origin. If the iframe does not contain the runtime or the configured origin is wrong, the connection does not become ready.

The user clicks **Apply inside the iframe**. `onApply` receives normalized API input, including structured edits, optional instructions, and converted screenshot data URIs. It does not receive a `chatId`; your application associates the bridge with the chat it is authorized to edit.

Return `true` or complete the callback without a return value to acknowledge success. Return `false` to reject Apply quietly, or throw an `Error` to also notify `onError`. For the synchronous backend below, acknowledge only after generation completes successfully. After saving, refresh your conversation history and preview through your normal application integration; the bridge does not fetch those resources for you.

### Validate and save on your backend

Authenticate your application user and authorize their write access to the chat **before calling this helper**. Your application's chat/user relationship must come from trusted server-side data, not ownership claims supplied by the browser. The v0 API key's scope does not replace your application's end-user authorization.

```typescript title="save-design-edits.ts"
import { parseDesignModeMessage, v0 } from 'v0'

async function saveDesignEdits(chatId: string, body: unknown) {
  const message = parseDesignModeMessage(body)
  const result = await v0.messages.send({ chatId, ...message })
  if (result.error) throw new Error(result.error.message)
  if (result.data.finishReason === 'error') {
    throw new Error('Design Mode generation failed')
  }
  return result.data
}
```

Expose this helper through your own authenticated endpoint, such as `/api/chats/{chatId}/design-mode`, and return the completed assistant message as JSON. `parseDesignModeMessage` validates and copies only Design Mode fields, stripping unrelated fields such as model overrides or MCP server IDs. Invalid or non-actionable input throws `TypeError`; map it to a 4xx response. API-side validation and authorization still apply.

For streaming saves, consume the stream and check the final result before acknowledging Apply. For async saves, track the returned assistant message to completion: accepting a queued job is not confirmation that the source edits succeeded. Do not automatically resubmit after an uncertain network failure; inspect or resume the original generation.

### Control and observe the editor

Use these methods from your application's controls:

```typescript
bridge.setEnabled(false)
bridge.setEnabled(true)

const unsubscribe = bridge.subscribe((state) => {
  console.log(state.connected, state.enabled, state.applying)
})

const layer = bridge.getState().tree?.children[0]
if (layer) {
  await bridge.selectNode(layer.id)
  await bridge.hoverNode(layer.id)
  await bridge.clearHoveredNode()
  await bridge.setNodeVisibility(layer.id, true)
}
```

`getState()` returns an immutable snapshot with `connected`, `enabled`, `applying`, `selection`, and `tree`. Layer controls use IDs from the current tree, not CSS selectors. These IDs are local to the current iframe document; the bridge clears selection/tree snapshots when the runtime reconnects. Visibility changes are pending visual edits and require Apply to persist into source.

The bridge restores the desired enabled state after iframe navigation. It rejects concurrent Apply requests and never automatically resubmits a save. There is one bridge per iframe. Parent-to-runtime control calls use a 30-second timeout by default (`timeoutMs`); this is not a timeout for model generation in `onApply`.

On component teardown, or before switching chats or preview origins, clean up the subscription and bridge:

```typescript
unsubscribe()
bridge.dispose()
```

`dispose()` disables the editor, removes listeners, closes the connection, rejects pending control calls, and aborts the signal passed to `onApply`. Aborting a browser fetch does not guarantee cancellation of a generation your backend already accepted.

The helper does not provide parent-side Apply, undo/redo, or screenshot-capture methods. Apply comes from the runtime's own editor button. For custom headless controls, submit the structured input below through your backend.

## Submit headless edits

```bash
curl -X POST "https://api.v0.dev/v2/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $V0_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "designMode": {
      "edits": [{
        "type": "style",
        "element": {
          "selectorPath": "main > h1",
          "tagName": "h1",
          "textContent": "Welcome",
          "reactComponentName": "Hero"
        },
        "changes": {
          "fontSize": { "from": "32px", "to": "48px" },
          "textContent": { "from": "Welcome", "to": "Hello there" }
        }
      }]
    }
  }'
```

The response is the normal assistant message, including file edits and usage. Use `GET /chats/{chatId}/files` to retrieve the updated source or [get the preview URL](/docs/api/v2/guides/accessing-previews) to inspect the result.

### Supported edits

`designMode.edits` accepts up to 100 operations:

| Type            | Fields                                      | Purpose                                                                                                                                                                                   |
| --------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `style`         | `element`, `changes`                        | Change CSS properties or text. Use camelCase CSS names such as `backgroundColor` or `objectFit`; use `textContent` for text replacement. Each property has string `from` and `to` values. |
| `deleteElement` | `element`                                   | Remove the element from the source.                                                                                                                                                       |
| `move`          | `element`, `position`, destination selector | Move the element. For `inside`, supply `toParentSelector`. For `before` or `after`, supply `targetSelector`.                                                                              |
| `fontInjection` | `fontName`                                  | Add a font to the project. Combine with a `style` edit to apply it.                                                                                                                       |

An `element` requires `selectorPath` and `tagName`. Optional `id`, `classNames`, `textContent`, and `reactComponentName` provide additional source-location context.

## Send scoped instructions and screenshots

Use `message` for instructions, `designMode.element` to scope them to a CSS selector, and optionally `designMode.screenshot` for visual context. Instructions can accompany structured edits or be used on their own when an element or screenshot is supplied.

```json
{
  "message": "Replace this image with the attached asset and preserve its dimensions",
  "designMode": {
    "element": "img.hero",
    "screenshot": {
      "url": "https://example.com/current-hero.png",
      "anchorElement": "main > section.hero"
    }
  },
  "attachments": [{ "url": "https://example.com/replacement.png" }]
}
```

Screenshots accept HTTPS image URLs or base64 image data URIs. Supported formats are PNG, JPEG, WebP, and GIF. The screenshot counts toward the existing limit of 20 attachments per request and 10 MB per attachment. Other assets can be supplied through `attachments`.

Image-related CSS and scoped image instructions use the same source-editing flow. Design Mode does not expose a separate image regeneration operation. Existing `modelConfiguration.imageGenerations` remains available for image generation requests.

## Convert raw runtime payloads

If you implement your own editor or runtime integration, `toDesignModeMessage` is available from both `v0` and `v0/browser`:

```typescript
import { toDesignModeMessage } from 'v0/browser'

const message = toDesignModeMessage({
  edits: [{
    type: 'style',
    element: { selectorPath: '#heading', tagName: 'h1' },
    changes: { fontSize: { from: '32px', to: '48px' } },
  }],
  instructions: { message: 'Only change this heading', element: '#heading' },
})
```

The converter preserves supported edit types, combines instructions and edits into one message, omits empty instructions, and converts the runtime's JPEG region/element screenshots into `designMode.screenshot.url` data URIs. It preserves region screenshot anchors and does not mutate the original payload. `createDesignModeBridge` already performs this conversion before calling `onApply`; do not convert its normalized message again.

Use `parseDesignModeMessage` on your backend to validate normalized messages, regardless of whether they came from the bridge or your own frontend. Payload helpers do not make network requests or authenticate users.

## Streaming and async execution

The same body works with all message send endpoints:

- `POST /chats/{chatId}/messages`: wait for the completed assistant message.
- `POST /chats/{chatId}/messages/stream`: receive the existing message SSE events.
- `POST /chats/{chatId}/messages/async`: receive `202` with `messageId`, then poll the message until `finishReason` is non-null.

For interrupted streams, [resume the existing generation](/docs/api/v2/guides/resuming-streams) rather than resubmitting the edits.

## Behavior and restrictions

- A nonempty `message` is required unless at least one structured edit is supplied. Without structured edits, Design Mode instructions also require an element selector or screenshot.
- Design Mode chooses its own model automatically. `modelConfiguration.modelId` does not select the editing model. For streaming requests, the initial usage model is `null`; the final usage is authoritative.
- Normal credit billing applies. Design Mode is not a free editing endpoint.
- Existing chat write permissions and API v2 compatibility requirements apply. Legacy next-lite chats are not supported.
- Vercel OIDC project principals can submit structured edits and element-scoped instructions. Screenshots and other attachments remain unsupported for these principals.
- Design Mode applies to existing chats only. Create or import a chat first, then send the design edits.
- The headless API does not capture screenshots or discover elements. The browser bridge connects to the existing preview runtime rather than exporting a separate editor component.

---

For a semantic overview of all documentation, see [/docs/sitemap.md](/docs/sitemap.md)

For an index of all available documentation, see [/docs/llms.txt](/docs/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/docs/agents.md](/docs/agents.md)