CSS Inspector Mini
Click an element on any web page and get a chat-ready description of it in Claude Code or any other coding agent.
Let's talk about this element `a.dsnav-top-link:nth-child(3)` (a):
No more describing a button in prose, or hunting for the selector by hand. Point the extension at a running page, click the thing you mean, paste.
- Works on any HTTP or HTTPS page — your dev server, a staging link, a public site
- Never modifies your files
- The page opens inside the editor, so your app and the chat sit side by side
- Collect several elements and send them as one message
- Five wordings, from a discussion opener to a numbered feedback list
Contents
How it works
The extension runs a small local server and proxies the page you want to inspect. It fetches the page server-side, strips the headers that block embedding (X-Frame-Options, frame-busting CSP), injects a picker overlay, and serves the result back on loopback.
┌─ your editor ──────────────────────────────────────────────┐
│ │
│ ┌─ Inspector panel ─┐ ┌─ the page, proxied ──────────┐ │
│ │ target, selection │ │ …your app, with a picker… │ │
│ │ wording, actions │ │ [Pick] │ │
│ └───────────────────┘ └──────────────────────────────┘ │
│ ▲ │ │
│ └────── WebSocket ───────────┘ │
└────────────────────────────────────────────────────────────┘
Because the overlay is served from the same origin as the proxied page, the picker attaches to anything reachable — including sites that refuse to be put in an iframe. The server listens on the first free port in 53110–53199 and stops when the editor closes.
Getting started
- Start your app however you normally do.
- Open the CSS Inspector icon in the activity bar (the cursor-in-a-box).
- The panel shows a detected dev server. Ports
3000, 4200, 5173 and 8080 are probed automatically — if yours isn't there, paste a URL into the field in the switcher and press Enter.
- Click Pick an element. The page opens in the editor with the picker already armed.
- Click any element. The selector lands on your clipboard and the chat takes focus.
- Paste, add your question, send.
Nothing to arm, no mode to enter. Clicking Pick an element is the whole gesture.
The panel
The panel shows only what's true at the moment, so it changes shape as you work.
Idle — the current target, and one framed block holding the action and its destination: Pick an element with on localhost:4321 beneath it, plus a live dot so you can see the server is up. The label says what the button does, the line says where it goes, and the frame says they belong together. A ⌄ on the button opens the three things you can do with that address — copy the inspect link, copy the original address, or open it in your own browser.
Picking — an indicator with a live count and an ✕ to cancel. Esc works too.
Captured — what you picked, the wording pills, a preview of the exact text, and the action.
The preview is the text itself, character for character — what you read is what gets pasted.
Single and multi-select
A Multi-select switch sits under the primary button.
|
Off (default) |
On |
| One click |
Captures and copies |
Adds to a selection |
| After a pick |
Picking ends, page usable again |
Stays armed, keeps collecting |
| Clipboard |
Immediate |
Nothing until you press the button |
| Clicking a picked element again |
Replaces it |
Removes it from the selection |
With Multi-select on, picked elements get a green outline and a numbered badge in the page, matching their row in the panel — so you can tell which element note 2 refers to.
Turning it back off keeps your most recent pick and drops the rest.
Wordings
Five ways to frame the same selection. Switching re-copies immediately, so you can pick once and try wordings until one reads right. Your choice is remembered per workspace.
Discuss (default)
Let's talk about this element `.hero-title` (h1):
Restyle
Update the styles for these 3 elements: `.hero-title`, `.cta`, `footer .legal`:
Debug
Something looks wrong with this element `.hero-title` (h1). Here's what I'm seeing:
Selector — just the selector, nothing else. Good for pasting into code.
.hero-title
Feedback — a numbered scaffold you fill in after pasting.
Feedback on these 3 elements:
1. `.hero-title` (h1) -
2. `a.dsnav-top-link:nth-child(3)` (a) -
3. `footer .legal` (span) -
Containers read area rather than element, so a wrapper doesn't get described as a widget.
Choosing what to inspect
Click the target line at the top of the panel to open the switcher.
DETECTED
● localhost:3000 Acme Admin · Dashboard ⧉ ↗
● localhost:5173 Acme Marketing Site ✓ ⧉ ↗
● localhost:8080 Storybook ⧉ ↗
RECENT
○ staging.acme.io ⧉ ↗
─────────────────────────────────────────
[ Paste any URL, press Enter ]
- Detected servers were found listening. Recent are URLs you've opened before. They're separate groups, so a server you've never opened is never described as recent.
- Page titles are fetched when you open the menu — a port number can't tell an admin app from Storybook. Nothing is fetched at startup.
- Clicking a row switches the target. Clicking its ↗ doesn't — that opens the URL in your own browser and leaves the target where it was. Useful for checking which server is which, and the only way to see a signed-in page as yourself: the proxy forwards no cookies, so a webview inside the editor would show you the same signed-out page.
- ⧉ copies the plain address to your clipboard, with no wording applied and without touching the chat — for dropping somewhere yourself.
- Paste a URL and press Enter to go straight there. A rejected URL keeps its text and says why, in place.
- Recents are stored per workspace, so each project keeps its own list.
In the page
The proxied page carries a small Pick pill at the bottom right. You normally won't need it — arriving from the panel means the picker is already armed — but it toggles picking on and off if you want to browse first.
- Hover highlights the element under the cursor
- Click captures it
Esc steps out of picking so the page behaves normally
A snackbar confirms each pick. In multi-select it reports Added / Removed with a running count, and tells you the elements are waiting in the panel rather than claiming they're on your clipboard.
About the selectors
Selectors prefer an id, then a unique class, and fall back to a positional path anchored on the nearest ancestor id.
Two kinds of name are deliberately skipped, because a selector containing them is wrong tomorrow:
- Machine-generated names — CSS-module hashes, styled-components and emotion classes, React
useId values, anything with a random alphanumeric tail. Both of these are real, from one page: Primer_Brand__FormControl-module__FormControl___xyCI6 and FormControl--_R_9l8l_. This applies to ids as much as classes; a framework-minted id is no more durable than a hashed class, and it's tempting precisely because ids normally are reliable.
- Runtime state —
is-active, open, selected and friends. Unique right now, pointing at a different element the moment the user clicks something else.
A third case gets qualified rather than dropped. A class with no letters in it describes nothing — Webflow prefixes numeric class names with an underscore, so a heading arrives as ._2. That's unique and stable but unreadable, so it borrows its parent for context: section.hero-section > h2._2.
Two rules keep paths short. Each step carries at most one class, and :nth-child is added only where a step is genuinely ambiguous among its siblings — an index on an element its class already identifies says nothing and breaks the moment anything is inserted above it.
A positional fallback is uglier than a hash but survives a rebuild, which is the trade being made.
Commands
| Command |
What it does |
| CSS Inspector Mini: Pick element |
Asks which URL, then opens it with the picker armed |
| CSS Inspector Mini: Pick element on current target |
Skips the question and reuses the known target |
| CSS Inspector Mini: Stop inspecting |
Removes the picker from every open page and closes the editor's browser panel |
A Pick element item also sits in the status bar. The inspector server keeps running either way — there's nothing to start or stop by hand.
Settings
cssInspectorMini.browserMode
"embedded" | "external" — default "embedded"
Where the page opens. embedded keeps it in the editor beside your chat. external uses your default browser; the picker still attaches, because the page comes through the proxy either way.
cssInspectorMini.chatInsertMode
"replace" | "append" — default "replace"
append prefixes the copied text with a newline, so it drops cleanly below something you've already typed in the chat.
cssInspectorMini.defaultFraming
"discuss" | "restyle" | "debug" | "selector" | "feedback" — default "discuss"
Which wording to start from. Once you pick a different one in the panel, that choice is remembered per workspace and takes precedence over this.
cssInspectorMini.recentUrlsLimit
number, 1–50 — default 10
How many recent URLs to keep. Stored per workspace.
Known limitations
Signed-in pages render signed out. The proxy fetches server-side and forwards no cookies. Use the ↗ in the switcher to open the page in your own browser — you'll see it as yourself, though the picker won't attach there.
Single-page apps may have broken API calls. Relative URLs resolve through an injected <base href>, which covers images, stylesheets and scripts but not fetch('/api/…') calls made from JavaScript.
Clicking a link inside a proxied page navigates out of the proxy. Use the panel to switch pages.
Non-HTML responses pass through untouched, so no picker appears on them.
Troubleshooting
The page opened in an external browser instead of the editor.Some editors — Cursor among them — don't ship VS Code's Simple Browser. The extension tries the known editor browsers and then falls back to rendering the page in its own panel, so this should be rare. If it happens anyway, the page still works and the picker still attaches.
No Pick pill on the page.The response probably wasn't HTML. Non-HTML content is proxied unchanged and gets no overlay.
The panel says not responding.The extension couldn't open a TCP connection to that host and port. If it says not checked, it simply hasn't looked yet — it won't claim a server is down without evidence.
Nothing happens when I click an element.Check the pill reads Picking…. If it says Pick, picking was cancelled — click it, or run Pick element again.
Development
npm install # dependencies
npm run compile # type-check and build to out/
npm run watch # rebuild on change
npm test # vitest
npm run package # build a .vsix
Press F5 to launch an Extension Development Host. It compiles first, so you're never debugging a stale build. A new activity bar icon only appears in a freshly launched host — reloading an existing one won't pick up manifest changes.
Layout
| Path |
Responsibility |
src/extension.ts |
Activation, commands, status bar, opening the browser |
src/inspector-server.ts |
The HTTP + WebSocket server, proxying and messaging |
src/proxy-transform.ts |
Pure HTML transform: base href, CSP strip, overlay injection |
src/sidebar-view.ts |
The panel |
src/target-state.ts |
Pure view-model: which targets to offer and how to group them |
src/framings.ts |
Pure: how a selection is worded |
src/page-titles.ts |
Fetches page titles for the switcher |
src/workspace-probe.ts |
Port probing and reachability |
src/recent-urls.ts |
Per-workspace recent URL store |
src/overlay/overlay.js |
The in-page picker |
The pure modules carry the test suite; the server and the panel are verified by the manual matrix in SMOKE.md, since neither can run outside an editor host.
Design notes and implementation plans live in docs/superpowers/.
A note on the overlay script URL
proxy-transform must emit an absolute <script src> for the overlay. The <base href> it injects points at the inspected site, so a root-relative src would make the browser fetch overlay.js from that origin and the picker would never load. This shipped broken in v0.2.0 and v0.2.1; two regression tests now guard it.