Skip to content
dev-only, zero dependencies

Edit your app in the browser.
Let your agent write the code.

Point at any element, tweak it, and hand your agent a JSON diff it can actually apply.

pnpm add -D @guerrerosoconm/visual-editor
dependencies
0
licence
MIT
version
v0.4.0
hosts
React, Vue, Angular, vanilla

Four steps, and none of them is describing the change in a prompt.

Pointing is faster than writing a paragraph about what you were pointing at. The editor's whole job is turning a point into something an agent can act on without guessing.

  1. 1

    Hover

    Every element highlights with its tag, its component and its size. Walk the tree with the arrow keys when the thing you want is three divs up.

  2. 2

    Tweak

    Click, and the panel opens. Spacing, fill, stroke, type, shadows, hover states. Changes preview as inline styles, so nothing is committed yet.

  3. 3

    Save

    Every touched element is written to edits.json with the before, the after, and enough anchors to find the JSX that produced it.

  4. 4

    Hand it over

    Your agent reads the file, finds the source, and writes the change in your styling system. Tailwind, CSS modules, whatever you actually use.

It boots from one call in anything.

The editor injects its own styles, paints into its own root and reads nothing from the host. What changes between frameworks is only how much of what it finds it can put a name to.

The editor, per host
host component names in the payloadlive props read for the variant editclass, testid and selector anchorssource stamping
React yes yes a yes yes
Vue 3 yes yes a yes no b
Angular yes yes a yes no b
Svelte, Solid, plain HTML no no yes partial c

The payload is complete on every row. Only the naming layer varies: without component names an agent gets the class, the test id, the selector and, on JSX, the file and line. That is still the whole address.

The endpoint, per server
host edits.json on diskcomponent manifesthow you get the batch
Next.js App Router yes yes written on save
Remix yes yes written on save
Hono yes yes written on save
Astro yes yes written on save
plain Node yes yes written on save
no backend at all no no console [AI-EDIT-REQUEST], or Copy JSON
a
Read from dev-build internals to seed the Component dropdowns. It reaches the payload as component.props, which needs a cva or tv manifest, or one you supply.
b
The plugin's default include is /\.[jt]sx$/ and it parses JSX, so a .vue single-file component and an Angular template are not stamped. Everything else on the row is unaffected.
c
The stamper is framework-blind: it rewrites any lowercase host tag in a .jsx or .tsx file, so a Solid app gets stamps. A .svelte file or a hand-written .html page gets none.
vue, svelte, astro, plain html
import { mountVisualEditor } from '@guerrerosoconm/visual-editor/standalone';

if (import.meta.env.DEV) mountVisualEditor();

The server adapter returns Web-standard Request to Response handlers, so Next.js App Router, Remix, Hono, Astro and plain Node all work from the same three exports.


the part worth caring about

A truer payload beats a richer panel.

Anyone can capture a pile of computed pixels. The difference between an agent that applies your change and one that guesses at it is whether the JSON carried the intent or only the fallout.

pixels, so the agent has to guess
"styles": {
  "background-color": {
    "before": "rgb(15, 23, 42)",
    "after":  "rgb(239, 68, 68)"
  },
  "color": {
    "before": "rgb(248, 250, 252)",
    "after":  "rgb(255, 255, 255)"
  }
}
intent, so the agent just applies it
"component": {
  "name": "Button",
  "file": "components/ui/button.tsx",
  "props": {
    "variant": {
      "before": "default",
      "after":  "destructive"
    }
  }
}

You get that second shape when your components use cva. The server scans them, the panel gets real variant and size dropdowns, and switching one swaps the actual classes on the element. A genuine variant, not a CSS impression of one.


Stop making the agent search for your JSX.

Add the Vite plugin and every element carries the file, line and column of its own tag. The agent stops grepping and starts being handed the answer.

Dev-only structurally: the plugin declares apply: 'serve', so a production build never runs it whatever your config says. Still zero dependencies, because it parses with the TypeScript already in your project, loaded lazily. And nothing regresses without it. The payload anchors on component ancestry and class names exactly as before.

vite.config.ts
import { visualEditorSource } from '@guerrerosoconm/visual-editor/vite';

export default defineConfig({
  // Before react(): the JSX has to still be there to be stamped.
  plugins: [visualEditorSource(), react()],
});
every edit now carries this
"source": "src/components/Button.tsx:42:7"

One saved element, every field, and what it buys your agent.

This is the artefact the whole tool exists to produce, complete and not trimmed for the page. Point at any group of fields to see the lines it owns, or at a line to see what it is for.

edits.json, one entry of 26 lines
{ "timestamp": "2026-07-28T09:14:02.481Z", "url": "http://localhost:5173/dashboard", "viewport": { "width": 1440, "height": 900 }, "selector": "[data-testid=\"project-card-cta\"]", "tagName": "BUTTON", "className": "inline-flex items-center rounded-md px-3 py-2", "testId": "project-card-cta", "source": "src/components/ui/button.tsx:42:7", "sourceAmbiguous": true, "componentStack": ["Button", "ProjectCard", "DashboardPage"], "styles": { "padding-top": { "before": "8px", "after": "12px" }, "padding-bottom": { "before": "8px", "after": "12px" } }, "states": { "hover": { "background-color": "rgb(29, 78, 216)" } }, "text": { "before": "Open project", "after": "Open" }, "note": "Match the spacing scale we use on the hero CTA.", "component": { "name": "Button", "file": "src/components/ui/button.tsx", "props": { "variant": { "before": "default", "after": "destructive" } } }}

The panel exists to make three things sayable.

Layout, spacing, appearance, fill, stroke, type and effects, as you would expect from any editor. These three are the ones that change what the payload can carry.

The full styles panel: breadcrumb, state tabs, component variants, layout, spacing, fill, stroke, typography, effects and the element's text. Three callouts point at the state tabs, at the variant and size dropdowns, and at the Save edit button.
  1. one panel per state: hover, focus, active
  2. these are real cva variants, not class guesses
  3. Save edit writes the entry to edits.json

State tabs

Default, :hover, :focus and :active. Edit with one selected and it exports under states.hover rather than styles. A hover style used to be unsayable.

"states": { "hover": { "background-color": "rgb(29, 78, 216)" } }

Undo that tells the truth

Page-wide and in the order you did things, so undoing something on another element selects it and shows you. Saving does not clear the history, and undo never reaches into edits.json.

the element goes dirty again, which is what actually happened

Notes, pinned

Leave the agent a message about why. Every element carrying one gets a numbered pin on the page, because a note is the one edit that changes nothing visually.

"note": "Match the spacing scale we use on the hero CTA."


quickstart

Three steps and you are editing.

1 . install
pnpm add -D @guerrerosoconm/visual-editor
2 . mount it, behind a dev check
// app/layout.tsx
import { VisualEditor } from '@guerrerosoconm/visual-editor';

export default function RootLayout({ children }) {
  return (
    <html><body>
      {children}
      {process.env.NODE_ENV === 'development' && <VisualEditor />}
    </body></html>
  );
}
3 . expose the endpoint
// app/api/dev/visual-edits/route.ts
import { createVisualEditsHandlers } from '@guerrerosoconm/visual-editor/server';

export const { GET, POST, DELETE } = createVisualEditsHandlers();
!

The NODE_ENV guard is what keeps the editor out of your production bundle, and the endpoint 404s outside development. Do not skip it. Add edits.json to your .gitignore while you are there.

then hand this to your agent
Read edits.json. For each edit, locate the element in the source using
source (when present), className, testId and componentStack, translate the
style values into our Tailwind classes, apply the changes, including any
states.hover / states.focus / states.active as the hover:/focus:/active:
variants, then clear the file.

The questions that decide it.

Does this ship to production?

Only if you mount it unconditionally, which you should not. The endpoint independently 404s outside development.

Does it modify my source?

No. It writes edits.json and nothing else. Your agent makes the code changes, and you review that diff like any other.

Why do I get pixels instead of my Tailwind classes?

Because the browser only knows computed values. That is why every diff carries before, so the agent maps before: 12px to the p-3 in your JSX and picks the right target class.

I do not use cva. Does component detection still work?

The Component section simply does not appear, and everything else works. You can also supply the manifest yourself. Check the console first: every definition the scanner had to drop is logged there with the file and the reason.

My edits disappeared.

Closing the editor discards what you did not save, but it asks first, and so does the browser if you reload with anything pending. A full page reload does clear everything, deliberately: after your agent edits the source, the page should show the source.

What does it cost?

Nothing. MIT, zero runtime dependencies, no account and no telemetry. React and react-dom are peers, nothing else.

  • 0 runtime dependencies react and react-dom as peers
  • MIT licence yours to fork
  • none telemetry no account, no external host

Built and maintained by Marcos Guerreros Ocón. One person, in the open.

How it is built What is not done yet

Point at it. Ship it.

npm GitHub