DEV Community

Andrew Gabaraev
Andrew Gabaraev

Posted on

I built a Vite plugin that writes CSS changes back to your source files

Every time I wanted to tweak a margin or color during development, I'd do the same thing:
open DevTools → find the element → adjust the value → like it → copy it → switch to my editor → find the right file → paste it.

That's 7 steps for a one-line change. I built LiveStyleSync to make it one.


What it does

LiveStyleSync adds a small panel to your Vite dev environment. Click any element on the page, edit its CSS properties in the panel, and the change writes directly to your source file. Vite HMR picks it up instantly.

No copy-pasting. No switching tabs.

Click element → edit value → Vite HMR updates browser → source file updated
Enter fullscreen mode Exit fullscreen mode

Quick start

npm install livestylesync-overlay livestylesync-vite-plugin
Enter fullscreen mode Exit fullscreen mode
// vite.config.ts
import { liveStyleSync } from "livestylesync-vite-plugin";

export default defineConfig({
  plugins: [liveStyleSync()],
});
Enter fullscreen mode Exit fullscreen mode
// main.ts
import { mount } from "livestylesync-overlay";

if (import.meta.env.DEV) {
  mount();
}
Enter fullscreen mode Exit fullscreen mode

That's it. A panel appears in the corner of your app.


How it works under the hood

This was the interesting part to build.

Reading styles: The overlay traverses document.styleSheets (the CSSOM) to find every CSS rule that matches the clicked element — including rules inside @media, @container, and pseudo-state rules like :hover. It resolves which source file owns each rule via Vite's data-vite-dev-id attribute.

Writing back: When you apply a change, it goes over WebSocket to the Vite plugin. The plugin uses PostCSS to parse the source file as an AST, find the exact rule and declaration, patch the value, and write the file back. PostCSS handles all the edge cases — complex selectors, nested rules, SCSS nesting syntax.

Edge cases I had to handle:

  • Universal selectors (*, ::before) matching every element — had to skip those
  • CSSContainerRule not being in TypeScript's lib — duck-typed via conditionText property
  • Inline styles overriding class styles after restore — had to explicitly clear them
  • HMR timing after file write — replaced hardcoded setTimeout(400) with a server-sent patched confirmation

Features

  • @media and @container tabs — separate tab per breakpoint/container
  • Pseudo-state editing:hover, :focus, :active (uses CSS class injection trick)
  • CSS custom properties — browse and edit :root variables live
  • SCSS $variables — server scans all .scss files, lets you edit $primary etc.
  • Session history — git-style diff of all changes, undo by batch
  • Create new rules — add CSS to elements that have no source rule yet
  • Tailwind detection — warns instead of trying to patch generated utilities

CSS format support

Format Supported
Plain .css
.scss
CSS Modules
Vue <style scoped>
Tailwind utilities ⚠️ detected, warns

Works with any framework on Vite

React, Vue, Nuxt, SvelteKit, Astro, Solid — anything that uses Vite as the dev server. The overlay has no React peer dependency (Preact is bundled and isolated).


Try it

GitHub: https://github.com/Artyx71/livestylesync

npm:

npm install livestylesync-overlay livestylesync-vite-plugin
Enter fullscreen mode Exit fullscreen mode

I'd love feedback — especially if you try it on a non-React setup or hit an edge case with your CSS structure. Open an issue or drop a comment here.


Top comments (0)