UI Bridge

The missing link between your browser and your agent.

How it works

  1. Pick any element – hold Alt-Shift and click anything on your page to open a comment thread.
  2. Your agent responds – it reads the comment via MCP and suggests a code change or a live Tweak.
  3. React in the thread – reply, tweak values interactively or resolve the thread.

https://github.com/user-attachments/assets/634b3570-a4cc-4915-8094-aefde67a02ac

Setup

1. Install

Vite
# pnpm
pnpm add -D @ui-bridge/unplugin @ui-bridge/mcp
# npm
npm install --save-dev @ui-bridge/unplugin @ui-bridge/mcp
# yarn
yarn add -D @ui-bridge/unplugin @ui-bridge/mcp
// vite.config.ts

import { defineConfig } from 'vite';
import { uiBridgeVite } from '@ui-bridge/unplugin';

export default defineConfig({
  plugins: [...uiBridgeVite()],
});
Astro
# pnpm
pnpm add -D @ui-bridge/astro @ui-bridge/mcp
# npm
npm install --save-dev @ui-bridge/astro @ui-bridge/mcp
# yarn
yarn add -D @ui-bridge/astro @ui-bridge/mcp
// astro.config.mjs

import { defineConfig } from 'astro/config';
import uiBridge from '@ui-bridge/astro';

export default defineConfig({
  integrations: [uiBridge()],
});
Next.js
# pnpm
pnpm add -D @ui-bridge/next @ui-bridge/mcp
# npm
npm install --save-dev @ui-bridge/next @ui-bridge/mcp
# yarn
yarn add -D @ui-bridge/next @ui-bridge/mcp
// next.config.mjs

import { withUiBridge } from '@ui-bridge/next';

export default withUiBridge({
  // your Next.js config
});
Nuxt
# pnpm
pnpm add -D @ui-bridge/nuxt @ui-bridge/mcp
# npm
npm install --save-dev @ui-bridge/nuxt @ui-bridge/mcp
# yarn
yarn add -D @ui-bridge/nuxt @ui-bridge/mcp
// nuxt.config.ts

export default defineNuxtConfig({
  modules: ['@ui-bridge/nuxt'],
});

Using a stack without automatic source annotation? See Custom Source Annotation to configure it manually.

Using Laravel/Inertia, Storybook, or other technologies? See Custom Integrations for worked examples.

2. Connect your agent

UI Bridge exposes an MCP server — your agent reads your feedback and applies changes through it.

VS Code
// .vscode/mcp.json

{
  "servers": {
    "ui-bridge": {
      "type": "stdio",
      "command": "npx",
      "args": ["--no", "ui-bridge-mcp"]
    }
  }
}
Claude Code
// .mcp.json (project root)

{
  "mcpServers": {
    "ui-bridge": {
      "type": "stdio",
      "command": "npx",
      "args": ["--no", "ui-bridge-mcp"]
    }
  }
}
Cursor
// ~/.cursor/mcp.json

{
  "mcpServers": {
    "ui-bridge": {
      "type": "stdio",
      "command": "npx",
      "args": ["--no", "ui-bridge-mcp"]
    }
  }
}

Run your dev server. Alt-Shift-click any element to open a comment thread — your agent picks it up through the MCP server and creates Tweaks on demand.

Working in a monorepo or a subdirectory? See Monorepos and subdirectories to configure a custom working directory.

Documentation

Preferences

Plugin-level

Pass a preferences object to your integration to set defaults for your whole project. These are used as the base layer and can still be overridden by individual users at runtime.

// vite.config.ts

import { defineConfig } from 'vite';
import { uiBridgeVite } from '@ui-bridge/unplugin';

export default defineConfig({
  plugins: [
    ...uiBridgeVite({
      preferences: {
        commentBar: { position: 'bottom-right' },
        visibility: { status: 'always' },
      },
    }),
  ],
});

The same preferences key is available on all integrations (withUiBridge, uiBridge(), @ui-bridge/nuxt).

Browser overrides

Click the gear icon (⚙) in the comment bar to open the preferences dialog. Changes are saved immediately to .ui-bridge/preferences.json and broadcast to all connected browser sessions.

API

interface VisibilityConfig {
  /**
   * Which comments/knobs are visible.
   * 'non-approved' hides threads whose tweaks are all accepted or discarded.
   * @default 'non-approved'
   */
  status?: 'always' | 'non-approved' | 'never';

  /**
   * URL-based filter. All false/unset means show all comments regardless of URL.
   * Bar default: all false (show across all routes — click to navigate).
   * Panel default: { path: true } (current page only).
   */
  route?: {
    /**
     * Only show comments whose pageUrl matches the current origin
     * (protocol + host + port).
     * @default false
     */
    domain?: boolean;

    /**
     * Only show comments whose pageUrl pathname matches the current page.
     * Bar default: false. Panel default: true.
     */
    path?: boolean;

    /**
     * Only show comments whose pageUrl query string matches the current page.
     * @default false
     */
    params?: boolean;
  };
}

interface UserPreferences {
  /**
   * Shared visibility defaults for both the comment bar and the comment panel.
   * Each context can override individually.
   */
  visibility?: VisibilityConfig;

  commentBar?: {
    /**
     * Where the comment bar is pinned on screen.
     * @default 'top-left'
     */
    position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

    /**
     * Visibility overrides for the comment bar.
     * Merged on top of `visibility`.
     * Bar-specific route default: all false (show comments from all routes
     * so users can click a badge to navigate to a different page).
     */
    visibility?: VisibilityConfig;
  };

  ui?: {
    /**
     * Visibility overrides for the floating comment panel.
     * Merged on top of `visibility`.
     * Panel-specific route default: { path: true } (current page only).
     */
    visibility?: VisibilityConfig;
  };
}

Custom Source Annotation

If possible, UI Bridge automatically detects to which file and even line a comment belongs using code-inspector. If your stack doesn’t support it — or you want to override it — you can read it from HTML comments or data attributes in the rendered markup.

HTML comments

Use htmlComments with a regex pattern. Useful for e.g. Laravel Blade with laravel-view-debug that wrap partials in comments like <!-- Start view: /path/to/file.blade.php -->. Set inspector: false when code-inspector can’t annotate your templates:

// vite.config.ts

import { defineConfig } from 'vite';
import { uiBridgeVite } from '@ui-bridge/unplugin';

export default defineConfig({
  plugins: [
    ...uiBridgeVite({
      inspector: false,
      sourceAnnotation: {
        // Each entry is a regex. Capture group 1 is the file path.
        // List multiple patterns if you mix frameworks — first match wins.
        htmlComments: [{ pattern: 'Start view: (.+?\\.blade\\.php)' }],
      },
    }),
  ],
});
Data attributes

Use dataAttributes if your build pipeline stamps location onto elements. Supports a single file:line:col attribute or separate file and location attributes:

// vite.config.ts

import { defineConfig } from 'vite';
import { uiBridgeVite } from '@ui-bridge/unplugin';

export default defineConfig({
  plugins: [
    ...uiBridgeVite({
      sourceAnnotation: {
        // Single attribute encoding "file:line:col"
        dataAttributes: [{ pathAttr: 'data-source' }],
        // Or split across two attributes:
        // dataAttributes: [{ fileAttr: 'data-file', locAttr: 'data-loc' }],
      },
    }),
  ],
});

Custom Integrations

Laravel / Inertia.js

Laravel serves the HTML response from the backend, so the client script is never injected automatically. Add import 'virtual:ui-bridge' to your JS entry file (it’s a no-op in production) and register the plugin:

// vite.config.ts

import { defineConfig } from 'vite';
import inertia from '@inertiajs/vite';
import laravel from 'laravel-vite-plugin';
import { uiBridgeVite } from '@ui-bridge/unplugin';

export default defineConfig(({ command }) => ({
  plugins: [
    laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'] }),
    inertia({ ssr: false }),
    // Dev-only tooling: its production build hooks are no-ops but still get
    // invoked for every module in the graph, adding needless build time.
    ...(command === 'serve' ? uiBridgeVite() : []),
  ],
}));
// resources/js/app.tsx

import 'virtual:ui-bridge';

No sourceAnnotation config is needed for Inertia pages/components — Code Inspector instruments .tsx/.jsx files like any other Vite/React project. Only reach for sourceAnnotation.htmlComments if you also render raw Blade views alongside Inertia (see Custom Source Annotation).

Storybook

Storybook’s Vite builder (@storybook/builder-vite) already loads and merges your project’s root vite.config.ts before running its own viteFinal — so a uiBridgeVite() registered there is automatically picked up for Storybook’s preview build too. No extra plugin registration needed in .storybook/main.ts:

// .storybook/main.ts

import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',
  viteFinal: async (config) => {
    // uiBridgeVite() from the root vite.config.ts is already merged in here —
    // do NOT add it again. Registering it twice injects the client script
    // (and opens the WebSocket connection) twice, giving you two floating
    // panels.
    return config;
  },
};

export default config;

Never load the client into .storybook/manager.tsx. Storybook renders two separate documents sharing one browser tab: the manager (sidebar/toolbar chrome) and the preview (your actual story, inside <iframe id="storybook-preview-iframe">). viteFinal/webpackFinal only ever affect the preview build — which is exactly where Alt-Shift-click picking needs to run, in the same document as the story markup. If the client is loaded into the manager’s document instead, picking can only ever “see” the <iframe> element as an opaque leaf — it can’t reach across into the iframe’s separate document, so every click just selects the iframe.

Monorepos and subdirectories

The MCP server finds its data by looking for a .ui-bridge/ folder, starting from its working directory and walking up. This works automatically when UI Bridge runs at your project root.

If UI Bridge runs in a subdirectory — a docs/ folder, a monorepo package, etc. — set cwd to that directory:

// .vscode/mcp.json

{
  "servers": {
    "ui-bridge": {
      "type": "stdio",
      "command": "npx",
      "args": ["--no", "ui-bridge-mcp"],
      "cwd": "${workspaceFolder}/packages/my-app"
    }
  }
}

Allow tweaks outside the project root

By default, tweaks can only modify files within the Vite/framework project root (the directory containing your vite.config.ts, or your app’s working directory in Next.js). In monorepo setups where tweaks need to target files in sibling packages, enable allowOutsideRoot:

// vite.config.ts

...uiBridgeVite({
  allowOutsideRoot: true,
})

The same option is available on all integrations (withUiBridge, uiBridge(), @ui-bridge/nuxt).

When using the standalone server directly, pass the --allow-outside-root CLI flag:

node node_modules/@ui-bridge/server/index.mjs --root ./packages/my-app --allow-outside-root

Working with Git

UI Bridge stores everything in a .ui-bridge/ folder at your project root. Deciding what to commit — and what to ignore — is a team decision.

What’s in .ui-bridge/

PathContentsCommit?
comments/*.jsonOne file per comment thread (selector, text, replies, knobs)Yes — this is your shared feedback
preferences.jsonUser-overridden preferences from the browser dialogNo — see below
scripts/*.mjsKnob transform scripts (the code that edits your source files)Yes — needed to replay tweaks

Preferences

There are two ways to share preferences with your team. Usually it’s best to set defaults in the plugin config and let individuals adjust as they like through the browser dialog — those changes are local and won’t affect anyone else.

The simplest way to keep preferences local is to gitignore the whole file:

# .gitignore

.ui-bridge/preferences.json
Option A — Plugin config (recommended for teams)

Set preferences in your plugin config. These are checked into version control as part of your vite.config.ts (or equivalent) and apply to everyone:

// vite.config.ts

...uiBridgeVite({
  preferences: {
    commentBar: { position: 'bottom-right' },
    visibility: { route: { path: true } },
  },
})
Option B — Commit `preferences.json` (not recommended)

If someone adjusts preferences via the browser dialog and you want to share that with the team, commit .ui-bridge/preferences.json. It overrides the plugin defaults for all users who pull it.

The two layers merge: plugin config is the base, preferences.json overrides it. Individual users can override again via the dialog — but those changes will be overwritten the next time they pull if you’re committing the file.

Comments and Knob scripts

Comment files in comments/ are plain JSON. Commit them and every team member sees the same open threads when they run their dev server. The agent reads them through MCP and can pick up where you left off.

Knob scripts in scripts/ are the transform functions your agent writes when replying with a live tweak. They’re referenced by comment threads, so if you commit comments you should commit their scripts too — otherwise teammates won’t be able to replay the tweaks.

If you prefer to keep the .ui-bridge/ folder entirely local:

# .gitignore

.ui-bridge/

In that case, comments and tweaks are personal and not shared across the team.

FAQ

Do I still need something like Claude Design now?

If you want to create design systems from scratch, don’t work primarily in existing codebases, or prefer a fully visual environment — maybe. For iterating on real UI in a real codebase, probably not.

Does this only work in the browser?

The browser is where you annotate — it shows your running app. The codebase must target the web, but the agent operates directly on your source files. UI Bridge is just the link between what you see in the browser and the code behind it.

How does UI Bridge know which file and line an element comes from?

For supported frameworks (Vite, Next.js, Nuxt), UI Bridge uses Code Inspector under the hood. It instruments your build so that every DOM element carries metadata pointing back to the exact source file and line. When you click an element, that location is attached to the comment automatically — no manual wiring needed.

For stacks that Code Inspector doesn’t support, you can provide the same information via HTML comments or data attributes. See Custom Source Annotation for details.

How much can actually be changed — just font sizes, or also layout?

Whatever you ask for. Small, targeted changes are more precise; larger structural changes are possible but can be less predictable. Support for larger refactors is planned.

What does a typical workflow look like?

There are several valid workflows: an agent scaffolds a first draft and you refine it visually; a developer builds something and a designer reviews and tweaks it directly; or you just vibe-code and iterate from there. UI Bridge fits wherever you need a fast loop between browser feedback and code changes.

How do Tweaks actually work?

Tweaks are string replacements in your source files — written by the agent as transform scripts. Nothing is runtime-only: you always see the real code change, not a patch applied in memory.

How are changes saved?

Accepting a Tweak writes the change to the source file permanently. From there, your normal workflow takes over — commit with Git or whatever fits your process.

Why is this open source?

Because in times of Coding Agents someone else would build it anyway. Open sourcing it means everyone can use it, learn from it, and build on top of it — whether that’s contributing to the core project, building integrations for other frameworks, or just using it as a reference for how to connect browser feedback to code changes.