| name | surface-visual-edit |
| description | Use when dialing in a precise visual value on a running web app — box-shadow, an exact color or color token, gradient stops, padding/margin/gap, border-radius, opacity, type size/weight/line-height — and describing it in words is the long way round. Surface is a deterministic visual editor that opens over the running app, takes direct-manipulation input (sliders, scrubs, color swatches, drag handles), and writes each change back to source (a Tailwind class, a CSS property, or a token) as a clean, formatting-preserving edit. Reach for it after a couple of failed attempts to hit an exact visual value by prose ("a bit softer", "slightly warmer"). Do NOT use it for structural/layout changes, copy, logic, or any change you can just write. |
Visual editing with Surface
Surface is a small, deterministic visual editor for web apps. It has no
intelligence of its own — it takes direct-manipulation input (sliders,
scrubs, color swatches, drag handles) and writes each change back to the
source file as a real, formatting-preserving edit. You hold the intent;
Surface is the hand tool you summon for the exact visual calls where a slider
beats a sentence.
Packages it uses
Everything Surface needs is published under the @designtools npm scope —
a closed set, no third-party installs:
@designtools/surface — the editor itself (run with npx)
@designtools/vite-plugin — Vite + React, and Remix
@designtools/next-plugin — Next.js
@designtools/astro-plugin — Astro
@designtools/svelte-plugin — SvelteKit
The first install and first launch will trip a permission prompt. That's
expected — this skill can't and shouldn't waive it. Confirm the packages are
@designtools-scoped and approve. To skip the prompt on future projects, the
person can add this to their own .claude/settings.json once:
{ "permissions": { "allow": ["Bash(npx @designtools/*)", "Bash(pnpm add @designtools/*)", "Bash(npm i @designtools/*)"] } }
When to reach for it
Summon Surface when the user is dialing in a precise visual value and
describing it in words keeps missing:
- Shadows — box-shadow offset, blur, spread, layered shadows
- Colors — picking or nudging an exact color, or a color token
- Gradients — stop positions and colors
- Spacing — padding, margin, gap by eye
- Border radius / border width
- Opacity
- Typography — size, weight, line-height, tracking on the scale
The trigger: you've tried two or three times to hit an exact value by prose
("a bit softer", "slightly warmer", "not that much") and it isn't converging.
That's the slider moment — hand it to Surface.
When NOT to
Do it yourself; don't summon Surface for anything better expressed as words
or structure:
- Structural / layout changes (add a section, make it three columns, reorder)
- Copy edits, data or logic changes, new components
- Any change where you already know the exact value — just write it
Surface never takes natural language. If the change is a sentence, it's your
job, not Surface's. That boundary is what lets Surface write deterministically.
Setup — do this for them, don't hand them a checklist
The person using this is usually a designer who won't touch config or source
files. Do the plumbing yourself, once:
-
Install the framework plugin if the project doesn't already have one.
Detect the framework from package.json and add the matching package,
wiring it into the build config:
- Next.js →
@designtools/next-plugin (wrap next.config with
withDesigntools)
- Vite + React / Remix →
@designtools/vite-plugin (add it before
the React plugin)
- Astro →
@designtools/astro-plugin (add to integrations)
- SvelteKit →
@designtools/svelte-plugin (add to the Vite plugins)
This injects data-source mapping and mounts the selection overlay. It's
dev-only and stripped from production builds. Each package's README has the
exact config snippet.
-
Make sure the dev server is running — start it if it isn't. Note the
port; it varies by framework (Vite 5173, Astro 4321, Next.js / Remix
3000). Read it from the running server rather than assuming.
-
Launch Surface from the project root, pointing it at that dev port:
npx @designtools/surface --port <dev-port>
The first npx run installs the package — allow ~5–10s before the editor
answers; that gap is the install, not a hang. Autodetection scans the common
framework ports (3000–3002, 5173/5174, 4321/4322, 4173, 8080),
so --port is usually unnecessary — pass it only for an unusual port. Other
flags: --tool-port <n> moves the editor off 4400; --components <dir> and
--css <path> override the (now automatic) component and stylesheet detection;
--help prints all of them.
Use the URL Surface prints, never a hardcoded 4400. The editor serves on
4400 by default, but if 4400 is busy — usually a Surface still running from a
different project — it moves to the next free port and prints a ▶ Open Surface:
line with the real URL. Send the designer there. If you want 4400 back, stop the
stale one first: pkill -f "@designtools/surface". Then they select the element
and nudge by eye; each change is written straight to source (a Tailwind class, a
CSS property, or a token) and the dev server hot-reloads.
Launching on someone's behalf
Usually you're doing this for a designer with no terminal open, so run both
processes yourself in the background — don't hand them commands to type:
- Start the dev server as a background process; wait until it answers on its
port.
- Launch Surface as a background process (
npx @designtools/surface --port <dev-port>).
- Poll the editor port, not the dev port. Wait for the editor URL Surface
printed (the
▶ Open Surface: line — 4400, or the next free port) to respond
before you tell them it's ready — on a first run that includes the install
delay above, so give it a few extra seconds.
- When they're done, stop both cleanly: match the Surface process on
@designtools/surface, and stop the dev server you started. Don't leave
either running.
Handing back
When the user is done, the changes are already in the source files —
there is no export or apply step. Stop Surface (Ctrl-C), then git diff to
see exactly what changed. The diffs are clean, single-property, human-readable
edits you can review, refine in code, or commit like any other change.
Known limitation — check var(--token) values in the diff. On projects
whose styles run on CSS custom properties (a generated token layer,
var(--badge) for fonts, and so on), review font-family and any other
var(--*) value after a session. A change to one property (weight or size,
say) can currently rewrite a neighbouring font-family to a resolved literal,
or fall back to an inline style={{…}} when a CSS-Module class was available.
Don't assume a property you didn't touch is untouched — if you see it, revert
that line; a var(--token) reference should stay a reference.
The boundary
- Surface writes deterministically. A slider value is exact and lands as a
precise AST edit — that is the whole reason to reach for it over describing
the change: no generation, no guessing, a diff you would accept.
- Surface holds no intelligence. You are the surface; Surface is the tool.
Don't expect it to interpret intent — give it a running app and let the
human manipulate exact values, then take the resulting source edits back.