Skip to content

Vite HMR for WordPress: Live Reloading in the Block Editor with Sage 11

Emnes
Lightning bolt icon representing Vite hot module replacement

Hot module replacement — commonly called HMR — is the single biggest developer experience improvement in modern frontend tooling. Edit a CSS file, save, and the browser reflects the change without a page reload. Edit a JavaScript module and the change applies while component state is preserved. For developers building custom WordPress themes, HMR shrinks the feedback loop from “edit, save, switch to browser, refresh, wait for PHP to rebuild, find where you were” to “edit, save, done.”

Until Sage 11, HMR for WordPress was theoretically possible but operationally painful. Sage 10’s Bud.js asset pipeline supported HMR on the frontend but struggled inside the iframed block editor. Sage 11’s move to Vite and the @roots/vite-plugin made HMR genuinely work end-to-end — frontend, editor, even site editor — with minimal configuration. This post covers how it all works, how to configure it, and how to debug the cases where it doesn’t.

Why HMR Matters for WordPress Theme Development

Traditional WordPress theme development has a slow feedback loop. The typical cycle:

  • Edit style.css (or a preprocessed SCSS/LESS file).
  • Save.
  • Wait for the build tool to rebuild.
  • Switch to the browser.
  • Refresh the page.
  • Wait for WordPress to render.
  • Scroll back to where you were testing.
  • Check the change.

At 5–15 seconds per iteration, developers lose hours per day to context-switching. With HMR, that drops to under a second: save, see, continue. Over the course of a project, the time savings are substantial — and the quality of the work often improves because designers can iterate faster.

For block editor work specifically, HMR is transformative. Block styles render inside an iframe, and without HMR, every CSS tweak requires a full editor reload — which loses the state of whatever block you were previewing. With HMR, you edit and see.

How Vite HMR Works

At a high level:

  • Vite runs a local dev server (on port 5173 by default) that serves your CSS, JavaScript, and compiled assets as ES modules.
  • The dev server opens a WebSocket connection back to the browser.
  • When you save a file, Vite determines which modules depend on that file.
  • Vite sends an update message over the WebSocket, containing the new module content.
  • The browser swaps the updated module in place, without reloading the page.

For CSS, HMR is straightforward — the browser can swap stylesheets atomically. For JavaScript, HMR requires the module to be “HMR-aware” (calling Vite’s import.meta.hot.accept()). Most modern frameworks (React, Vue, Svelte) handle this automatically.

The @roots/vite-plugin Integration

Vite doesn’t know anything about WordPress on its own. The @roots/vite-plugin package (currently v2.1.0, April 2026) is what bridges Vite and WordPress. It handles three things:

  • Dev-server integration with WordPress’s wp_enqueue_style / wp_enqueue_script. Instead of linking to compiled files in public/build/, WordPress enqueues URLs pointing at Vite’s dev server. In dev, assets come from Vite; in production, from built files.
  • Block editor HMR. The plugin handles the iframe complexity — it injects the HMR client inside the block editor’s iframe, proxies the WebSocket, and makes HMR work inside the editor just like on the frontend.
  • Manifest generation for production builds. At build time, Vite produces manifest.json mapping logical entry names to content-hashed output filenames, which the plugin reads to generate correct wp_enqueue_* calls.

Setting Up HMR in a Sage Project

Fresh Sage 11 projects have HMR configured out of the box. If you’re starting new, there’s nothing to set up:

composer create-project roots/sage your-theme
cd your-theme
pnpm install
pnpm run dev

Visit your site. You should see HMR working — edit resources/css/app.css, save, and watch the style update in-place without a reload.

Behind the scenes, the Sage vite.config.js configures:

  • @roots/vite-plugin — the WordPress bridge.
  • laravel-vite-plugin — Laravel’s Vite integration, which Acorn uses to track asset entries.
  • Two entry points: resources/css/app.css + resources/js/app.js for the frontend, and resources/css/editor.css + resources/js/editor.js for the block editor.
  • Tailwind CSS via its Vite plugin.

HMR Inside the Block Editor

The block editor runs in an iframe. Inside that iframe, the block editor loads editor.css and editor.js — and those assets, in dev, come from Vite’s dev server, with HMR enabled.

When you edit editor.css, the Vite dev server detects the change, sends an update over the iframe-proxied WebSocket, and the editor reflects the new style without losing your currently-selected block.

This is the part most developers don’t realize they need until they try developing custom block styles. Before Sage 11, editing block styles meant constantly switching tabs and hitting refresh. After Sage 11, you save and see.

Development vs Production

Sage’s @roots/vite-plugin handles the dev/prod switch automatically:

  • In development: WordPress enqueues assets from the Vite dev server (http://localhost:5173/resources/css/app.css). The dev server opens the HMR WebSocket. Browser gets live-reload.
  • In production: WordPress enqueues built assets from public/build/ with content-hashed filenames. No dev server, no WebSocket, no HMR — just regular cached static files.

The plugin decides which mode to use based on whether a Vite “hot file” exists (public/build/hot, written by the dev server on start, removed on clean shutdown). This is the same mechanism Laravel uses.

Common Setup Issues and Fixes

HMR Not Connecting

If the browser console shows [vite] connecting... indefinitely, the WebSocket can’t reach Vite. Common causes:

  • Vite dev server not running. Start it with pnpm run dev.
  • Wrong host. Vite’s default host is localhost. If you’re developing on a different hostname (e.g., site.test), configure Vite’s server.host in vite.config.js.
  • HTTPS mismatch. The dev server and the WordPress site need to be on the same protocol. Serve both over HTTP in dev, or both over HTTPS with a self-signed cert.
  • Firewall or CORS. Browser extensions or corporate firewalls occasionally block WebSocket traffic. Test in an incognito window with extensions disabled.

HMR Works on Frontend but Not in Block Editor

This is usually an iframe-proxy issue. Verify that:

  • @roots/vite-plugin is at v2.0+ (older versions had iframe bugs).
  • Your block editor isn’t running inside a meta-iframe (some page builders nest iframes, which breaks HMR).
  • Browser developer tools → Network tab → WS filter shows an open WebSocket connection from the iframe.

Changes Require a Full Reload

Some changes can’t be hot-swapped and force a full reload:

  • Blade template changes — templates render server-side, so Vite can’t hot-swap them. The dev server does a full reload for Blade changes via a file watcher, which is the next-best thing.
  • PHP file changes — same reason; PHP executes server-side.
  • Non-HMR-aware JavaScript — vanilla JS that isn’t module-aware usually falls back to full reload.

The Sage configuration includes a file watcher that triggers a full page reload on PHP changes, so you don’t have to manually refresh even for server-side edits.

Vite Dev Server Crashes on Start

Usually a port conflict (5173 already in use by a different project) or a Node version mismatch. Check:

  • lsof -i :5173 — is another process using the port?
  • node --version — Sage 11 requires Node 20+.
  • Delete node_modules and run pnpm install fresh.

Advanced: Customizing HMR Behavior

For edge cases, you can customize HMR behavior in vite.config.js:

  • server.port — change the dev server port if 5173 conflicts.
  • server.strictPort — error instead of falling back to a new port when 5173 is in use.
  • server.hmr.host — set the HMR WebSocket hostname explicitly for proxied setups.
  • server.watch — add directories to the file watcher (e.g., additional Blade template directories).

Most projects don’t need any of these. Start with defaults and only tweak if something breaks.

HMR with Tailwind CSS v4

Tailwind CSS v4 has its own Vite plugin that integrates with HMR. Save a utility class change and Tailwind’s JIT regenerates the CSS, Vite hot-swaps the resulting stylesheet, and the browser reflects the change instantly.

Two gotchas to know about:

  • Tailwind’s class scanner needs to see your templates. Make sure your Tailwind source paths in @source directives (v4 CSS-first) or content globs (v3) cover all your Blade files.
  • Dynamic classes break the scanner. Tailwind can’t detect class="{{ $dynamic }}". Either use safelists, or whitelist specific dynamic patterns.

Frequently Asked Questions

Does HMR work with WordPress multisite?

Yes. Vite HMR is indifferent to whether the WordPress install is single-site or multisite. Each subsite just loads the dev server’s assets normally.

Can I use Vite HMR with a non-Sage theme?

Yes, but it requires more setup. You’ll need to add @roots/vite-plugin and write your own wp_enqueue_* bridge, or use a lighter integration like esbuild. Sage’s value is that all this is done for you.

Does HMR affect production at all?

No. HMR only runs when the Vite dev server is running. Production builds (pnpm run build) produce standard static assets with no HMR code.

Can multiple developers use HMR on the same site?

Not on the same local Vite dev server. Each developer runs their own Vite instance on their own machine. If you have a shared staging server, HMR isn’t typically enabled there — staging uses production builds.

Will HMR work if I SSH-tunnel my dev environment?

Yes, with correct Vite configuration. Set server.hmr.host to your tunnel’s public hostname and make sure the WebSocket port (same as HTTP port by default) is tunneled. Some tunnel tools (ngrok, Cloudflare Tunnel) handle WebSockets natively; others need explicit config.

How does HMR compare to BrowserSync-style live reloading?

HMR is strictly better. BrowserSync does a full page reload on every change, losing state. HMR swaps individual modules in place, preserving state. For CSS, both feel instant; for JavaScript, HMR is dramatically better.

Fast Iteration Is a Force Multiplier

A fast feedback loop changes how developers work. Designers and developers can iterate side-by-side. Bugs become obvious because you notice the behavior change the moment you introduce one. UI tweaks that used to take half a day (guess, rebuild, wait, check, repeat) take ten minutes (save, see, done).

Sage 11’s Vite-powered HMR is, honestly, the feature that justifies the upgrade from Sage 10 for most teams. Combined with Tailwind v4 and Blade components, it produces a developer experience that feels more like working in a modern React app than a WordPress theme — but with the full WordPress backend underneath.

At Emnes, every Sage theme project we ship uses HMR in development. The productivity gain is real and compounds across every task. If you want help setting up Sage or troubleshooting HMR on a complex setup, get in touch.

Related reading: Sage 11 deep dive, building a production theme with Sage, and Sage vs Underscores vs block themes.