An inspector in the page, built on what the dev server knows of every class. In dev only, unless the app asks for it in a build too (below). Install @cssints/devtools as a dev dependency of the app and turn it on:
export default defineConfig({ plugins: [cssints({ devtools: true })] });Alt+Shift+C, or the cssints button in the bottom right corner, opens it. Hovering an element previews it, a click picks it; ↑ is the parent, ↓ the first child, ← and → the siblings; Esc stops picking, then closes. For the demo's action button, hovered in a 1300 px window, the panel shows:
button pick ↑ ×
CLASSES BY CALL
src/components/Showcase.tsx:72 ↗
._1t8gqy5 padding: 0.5rem _.a
._1r802m6 background-color: color-mix(…, black 15%) :hover _.b
._pb21ov background-color: color-mix(…, black 30%) @media (width >= 60rem) :hover _.c
._tw1yer outline: 2px solid var(--color-accent) :focus-visible (not now) _.b
PROPERTIES
background-color color-mix(in oklab, var(--color-accent), black 30%) ✓
._pb21ov:hover _.c @media (width >= 60rem) :hover
background-color: var(--color-accent) ._ijw0jd _.a (struck through)
background-color: color-mix(…, black 15%) ._1r802m6:hover _.b (struck through)
outline nothing applies now
outline: 2px solid var(--color-accent) ._tw1yer:focus-visible _.b :focus-visible
TOKENS
--color-accent rgb(29, 78, 216) color.accent
the initial value of its @propertyClasses by call. The element's classes, grouped by the css.* call that made them (cn(...), cx(...), a member), the call that covers most of them first; the link opens the file at that line through Vite's /__open-in-editor, and the call's source is shown under it. Each class shows its declarations, its conditions (green: holds now) and its layer; a marker shows its kernel (marker of kernel frame.core), a member's class its scope ((frame)); a class that is not cssints' is listed as such.
Properties. Per property, the winning declaration, what it beat (struck through) and what does not apply now, with the condition that fails. The winner is the highest by layer, then specificity, then the sheet's order, among the rules that apply: a rule applies when its selector matches the element now and each of its at-rules holds for it, which the browser decides (a probe sheet around the element, so @container, @container style(), @supports and @scope are evaluated where the element is). A shorthand counts for each of its longhands: padding beside a padding-left of a higher layer says except padding-left. Kernels and global rules in _.k, themes and looks in their layers are rules like the others.
Checked against the browser. Each winner is set inline on a childless clone of the element, in its place, and the clone's computed value must equal the element's: ✓. When it does not (✗), the panel says why: the element's style attribute, a rule of another sheet (a rule outside every layer beats every layer, cssints' too: .mine { color: … } in src/inspect.css beats it), or a running animation. A value that depends on the element's content (width: auto, grid tracks) is ?, not checked. A custom property is checked with its declaration as the page's sheet has it, since LightningCSS reprints it (0.04 as .04) and an unregistered one computes to its tokens as written (cssints-g3u1).
Tokens. Each custom property the element's rules read: its value on the element, its path (color.surface), and the rule that sets it on the element or the nearest ancestor (a theme, a look in @scope, a member's class).
How it gets the data. Over Vite's HMR channel, no endpoint: the overlay asks for the table (cssints:devtools:get) and the server builds it from its own records: the sheet as it serves it, each class's rule, plugin and tokens, and the place of every call. After each transform the server says the table is stale, and an open overlay asks again.
Nothing in a build, unless asked. devtools: true adds a plugin with apply: "serve"; vite build has no entry, no script and no event of it (packages/devtools/test/build.mjs builds the demo with the option on and searches the output).
In a build: devtools: "build". The same overlay on the built site, for a project whose code is public or whose readers want to see how a page is styled (this site has it on). The build writes the table once every module is transformed, as a JSON asset (assets/cssints-devtools-<hash>.json: the sheet, the classes, the tokens, the sites), puts the overlay in a chunk of its own, and adds a loader to the entry: a cssints button and Alt+Shift+C, under 1 kB. Nothing else loads until the first open; then the overlay and the table do, and the panel opens. The table is static, so there is no stale state and no update. A site is its file relative to the project, the line and the call's source, shown as text: there is no link to an editor, and none to a repository. This site's table is 70 kB (12 kB gzip), the overlay 190 kB (64 kB gzip), the entry grows by 0.85 kB (0.4 kB gzip). The option needs @cssints/devtools installed like the dev one; without it the page logs a warning in the console on the first open.
Ask OpenCode: opencode (dev only, off by default, cssints-ro4k, 2026-10-10). With a picked element the panel can have a prompt box: type a change, Enter sends. It is a second option, because it starts a coding agent in your repository and must be asked for by name:
cssints({ devtools: true, opencode: true });
// or: opencode: { agent: "build", skills: ["cssints"] }The dev server, never the page, talks to the OpenCode V2 service that is running on the machine (the one opencode serve --service and the desktop app register; found with Service.discover of @opencode/client, so the page holds no address and no credentials). It creates a session in the workspace root (the nearest folder up from the Vite root with a .git), sends the prompt, waits for the session to end, and tells the overlay the status over the same HMR channel (cssints:devtools:ask from the page, cssints:devtools:asked back: no route that another origin could post to). The box shows sending, then running · ses_…, then completed, failed or interrupted; with no service running it says so and keeps your text. @opencode/client is an optional peer dependency: install it in the app (the message names it when it is missing). The prompt is made of cssints' own data, not source markers added to the markup, and is bounded (8 call sites, 24 properties, 3 beaten declarations each, 12 tokens, a call cut at 400 characters; the server cuts the whole at 16000):
Change the UI of the running app as the instruction says.
The styles are written with cssints: each `css.*` call below produced the classes of the element,
so edit those calls (or the markup that uses them), not the generated class names.
Instruction: make it bigger
Element: <p#p>
DOM path: html > body > p#p
Text: hello
Classes: _aasmd7 _1t8gqy5
Styled by (file:line, the call as written):
- main.ts:5
color("rebeccapurple")
- main.ts:5
p(2)
Properties now (the winner, then what it beat):
- color: rebeccapurple from ._aasmd7 (_.a)
- padding: 0.5rem from ._1t8gqy5 (_.a)A build has none of it: the server side is in the apply: "serve" plugin, and devtools: "build" with opencode on still gives a loader without a way to ask (the dormant box is part of the overlay chunk, about 1 kB). The idea and the V2 client calls follow brendonovich/vite-plugin-opencode (MIT).
Its own styles do not touch yours. The overlay is a custom element with a shadow root and its own cssints sheet, built with the package: the page's rules do not reach it, and its classes are not in your sheet or in the table.
Limits. The proximity of @scope is not ranked (no two looks set one property on one element), and a logical and a physical property (margin-inline-start, margin-left) are two properties, as cn() treats them; an element inside another shadow root is picked as its host; a page that Vite does not serve as HTML (server rendering) gets no entry.