---
title: "Stylewright"
url: "https://greg.johnson.io/projects/stylewright"
kind: "project"
description: "A dev-only Vite plugin. Click an element on the running page, edit its component's CSS with a live preview, and the change is written back into the .svelte source. It can hand the selected element to a coding agent over MCP, and from 0.2.0 it works in front of any server-rendered app."
published: "2026-10-18T00:09:16.371Z"
modified: "2026-10-18T00:09:16.371Z"
tags: []
---

# Stylewright

A dev-only Vite plugin. Click an element on the running page, edit its component's CSS with a live preview, and the change is written back into the .svelte source. It can hand the selected element to a coding agent over MCP, and from 0.2.0 it works in front of any server-rendered app.

![The Stylewright panel docked beside the playground page, listing the parsed .btn rules from lib/Button.svelte with colour swatches and editable values](https://greg.johnson.io/media/images/53fbd697-6cc3-4884-888e-fab5c2d111bd.png)

Stylewright is a dev-only Vite plugin. I click an element on the running page, change a value in its component's CSS, and the change is written straight back into the ` <style> ` block of the ` .svelte ` file that owns it. I built it for the design work on the Paupack Hills website, where the alternative was tuning a value in DevTools and then copying it into the component by hand before a reload threw it away. This site uses it too.

**[Source and install on GitHub →](https://github.com/Greg-J/vite-plugin-stylewright)**

## Why DevTools can't do this

DevTools can save a CSS edit back to disk, but only when the file is a whole stylesheet: a ` .css ` served by a ` <link> `, or one a source map points at as a complete file. A Svelte ` <style> ` is a region of CSS inside a mixed file, so DevTools has nowhere to write it. Vite makes it worse in dev by serving every stylesheet as JavaScript that injects a ` <style> ` tag, which DevTools treats as read-only. So Stylewright owns the whole round trip itself: an overlay in the page captures the edit, and a dev-server-only endpoint patches the exact declaration back into the source.

## How a save works

1. **Which element.** Svelte's dev metadata records which component rendered each element. It's the same data the Svelte inspector uses, so pick mode can name the file before you commit to the click.
2. **Which rules.** The dev server reads that component, finds its ` <style> ` block, and parses it with PostCSS. By default the panel lists only the rules that style what you picked, including the ` @media ` rules that apply to it, instead of the whole stylesheet.
3. **The preview.** Editing a value repaints the page as you type. A colour opens a picker seeded with the colours the component already uses.
4. **The write.** Press Enter and the matched declaration is updated in the PostCSS tree, stringified, and spliced back at exact offsets with magic-string. Nothing else in the file moves: not the markup, not the script, not the CSS around it. Vite then repaints from source, so what's on screen is what's on disk.

Anything that could corrupt a file is refused rather than guessed. A save that would drop an at-rule from the source is refused. A payload containing ` </style> ` is refused, because that route splices text into the component verbatim and would otherwise be a way to inject source.

## Handing an element to an agent

Some changes aren't a CSS value. "Make this a loading state" is markup, a state and a style. The Ask AI tab hands the selected element, its source file and its parsed rules to one coding agent session over MCP, and the agent makes the change under its own permission and diff flow. Claude Code, Cline, OpenCode and Kimi Code are supported.

Three rules keep this honest:

- **The overlay never edits a file.** It queues text. Every write is the agent's, under the agent's own approval.
- **Nothing is pushed into a session.** An MCP server can't make an idle agent start working, so you tell the agent once to watch for Stylewright edits, and from then on it asks for work. Until it does, the panel says so instead of queueing a request nobody will claim.
- **Status is never invented.** The panel shows only what the agent reported. There is no timer that decides it probably finished, and no file watcher that guesses. Done, needs input, and the list of changed files all come from the agent.

Each of the four agents registers an MCP server differently, and their tool timeouts don't agree: Cline counts in seconds, OpenCode and Kimi Code in milliseconds, and the defaults are far too short for an agent that is waiting for work. The setup screen writes the right registration for the agent you pick, with the path filled in for your install.

## Apps that aren't Svelte

Version 0.2.0 added a mode for apps whose pages are built on the server: a Cloudflare Worker under wrangler dev, Express, Rails, anything that isn't Svelte on Vite. Nothing is installed in the app. The Stylewright MCP server puts the overlay in front of the app's dev server as a proxy and gives you a URL to open in place of the app's own. Your agent names the CSS sources the overlay may write, either ` .css ` files or a template literal in a JS file, and the same PostCSS patcher splices the changed bytes into them. A template literal with ` ${…} ` in it can't be a source at all, because its CSS is only known at runtime, and anything else the patcher can't do safely is refused with a message saying so.

## Keeping it safe

The dev server writes to your source tree, and any page in your browser can reach localhost by URL. So the API is gated, not just obscure:

- **Same-origin only,** and only for a ` Host ` that is an IP literal or ` *.localhost `. That's what stops a DNS-rebinding attack from defeating the origin check.
- **Writes need a header no ` <form> ` can set,** so a cross-site POST can't rewrite a component.
- **Path containment is checked twice,** lexically and again after resolving symlinks.
- **The agent routes need a loopback socket and a token.** The token lives under ` ~/.stylewright `, deliberately outside the Vite root, because a credential inside the served tree is handed to any unauthenticated GET.
- **None of it exists in a production build.** The plugin only applies while serving.

There is one outbound request, IBM Plex from Google Fonts for the overlay, and it can be turned off. There is no telemetry.

## What's next

- A fallback for Svelte apps where the dev metadata isn't available. Plain-HTML mode already matches an element to rules without any metadata, and extending that to ` .svelte ` files is what's left.
- Inline ` style="" ` attributes and ` https:// ` targets in plain-HTML mode.
- More than one ` <style> ` block per component, and nested rules.
- Bundling IBM Plex so the overlay makes no outbound request at all.
- Answering an agent's clarifying question from the overlay, which needs MCP elicitation.
- A light theme for the overlay.

## How it's made

I'm building Stylewright with Claude, Anthropic's AI, as a team. I set the direction and judge the result by using it on real pages. Claude builds, tests and hardens. When a vendor's docs and its binary disagreed about a timeout, we measured the binary.

![Pick mode: the card component outlined in purple, with a label naming the div, its classes and lib/Card.svelte, and a hint bar reading Click any element to edit its component styles](https://greg.johnson.io/media/images/305c13e0-08aa-45aa-8bc8-8e791031f99c.png)

Pick mode names the element and the file that owns it before you click.

![The colour picker open on the .btn background: a saturation field, hue and alpha sliders, a hex input, and a swatch row headed IN USE](https://greg.johnson.io/media/images/e20c4e83-b47e-4972-a26e-06820fd1a7a6.png)

The colour picker opens seeded with the colours the component already uses.

![The bottom of the Ask AI tab: a composer holding a typed loading-state instruction, a hint reading Enter to send, and a session chip reading no session linked beside the Send button](https://greg.johnson.io/media/images/a4d7981e-131b-45c9-9156-f722e4f5d4c7.png)

Ask AI: the instruction typed, and the chip saying no session is linked yet.

![Settings, Claude Code: four numbered setup steps, the first a copyable claude mcp add stylewright command with an absolute path filled in, and a connected session listed below with a Link button](https://greg.johnson.io/media/images/c77a0ffa-9064-4607-a87a-6f633ef754ec.png)

The setup screen generates the registration for your install. A checkout gets an absolute path instead of a package name.

![The Settings screen: four panel layout options, and toggles for click to select, DOM tree and focus the selection](https://greg.johnson.io/media/images/9cef89b1-0bd7-4412-97ca-102792b778d8.png)

Dock the panel left, bottom, right or floating, and show only the rules that style what you picked.

- **Works with:** Svelte 4+ on Vite 5+, including SvelteKit
- **Also:** Any server-rendered app, through plain-HTML mode
- **Parser:** PostCSS
- **Write-back:** magic-string, at exact offsets
- **Agents:** Claude Code, Cline, OpenCode, Kimi Code
- **Transport:** MCP, loopback only
- **Package:** vite-plugin-stylewright 0.2.0 on npm
- **Code:** TypeScript, 8,955 lines
- **Tests:** 451 passing (Vitest)
- **License:** MIT
- **Started:** June 2026
