PR
DEV

Implementing HMR for WordPress Themes with Vite [Instant CSS Updates]

When developing WordPress themes, do you find yourself reloading your browser every time you save an SCSS file just to check the changes? Gulp’s `watch` command, BrowserSync proxy settings, and complex Webpack configurations… With Vite, you can achieve the experience of seeing styles reflected in your browser the moment you save—without any of those “detours”—in just a few minutes.

In this article, I’ll explain the steps to integrate Vite into an existing WordPress theme (including child themes) and implement CSS HMR (Hot Module Replacement). Since this is based on the configuration of a Cocoon child theme I’m currently using, you can replicate it exactly.

Why Use Vite for WordPress Development?

In WordPress theme development, PHP is used to generate HTML, while CSS and JavaScript control the front end. When seeking to combine this setup with modern build tools, many articles have traditionally recommended Webpack or Gulp.

Vite takes a fundamentally different approach from these tools. During development, it serves individual files directly using ESM (ES Modules), and only bundles them with Rollup during production builds. In other words, the bundling process does not run during development.

What this means is that even as the number of files increases, the development server does not take longer to start up. With webpack, the server starts only after it has traced dependencies and generated the bundle, but Vite starts the server immediately and transforms only the requested files on demand.

Why HMR Is Blazing Fast

Vite’s HMR (Hot Module Replacement) is, quite literally, lightning-fast. If you edit a single line of SCSS, only the changed portion is updated in the browser without reloading the entire page. To put it in practical terms, the browser updates before you even finish pressing Ctrl+S to save.

With traditional tools like BrowserSync or Gulp LiveReload, the process was: detect file changes → compile → instruct the browser to reload → reload the entire page.Vite replaces the “compile → instruct the browser to reload → reload the page” part of this process with incremental updates delivered via WebSockets. Since a full page reload isn’t necessary, your scroll position and form input state are preserved while only the visual content is updated.

Differences from webpack and Gulp

ItemVitewebpackGulp BrowserSync
First launchHundreds of millisecondsA few seconds to over ten secondsA few seconds
HMR (when CSS changes)Instant (milliseconds)1–3 secondsReload (1–3 seconds)
Complexity of configuration filesSimpleComplex (many loaders/plugins)Moderate
ESM supportNativeVia transpilationNot supported
Production buildRollup (High-Speed)webpack (Stable)Manual task design

Requirements for Setup

The following items are required for this configuration:

  • Node.js (v18 or higher recommended)
  • A running local WordPress environment (Herd, Local, Docker, wp-env, etc.—anything works)
  • Access to the theme directory (child theme recommended)

While methods using wp-env or Docker are commonly introduced, this article uses an approach that works regardless of the type of local environment. Whether you use Herd, DBngin, or Docker, you can follow the same steps for installation.

Directory Structure

After integrating Vite, the child theme directory structure will look like this.

cocoon-child-master/
├── dist/                  # ビルド出力先
│   ├── .vite/
│   │   └── manifest.json  # アセット対応表
│   ├── main-xxxxx.js
│   └── main-xxxxx.css
├── main.js                # エントリーポイント
├── vite.scss              # SCSS(main.jsからimport)
├── vite.config.js         # Vite設定
├── package.json
├── functions.php          # Viteアセットの読み込みロジック
├── style.css              # WPテーマ必須ファイル
└── node_modules/

The key point is thatmain.jsserves as the entry point, and from there, SCSS isimport. Vite follows this import chain and treats both CSS and JS as targets for HMR.

STEP 1: Installing Vite and Updating package.json

Execute the following in the theme directory.

cd wp-content/themes/your-child-theme
npm init -y
npm install --save-dev vite vite-plugin-live-reload vite-plugin-mkcert

The roles of the packages to be installed are as follows:

  • vite: The core build tool
  • vite-plugin-live-reload: A plugin that triggers a full browser reload when PHP files are modified
  • vite-plugin-mkcert: A plugin that automatically generates an HTTPS certificate for the local development server

If you are using SCSS/Sass, you will also need to install a Sass compiler.

npm install --save-dev sass

package.jsonThescriptssection should be configured as follows.

{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

With this configuration,npm run devwill start the development server,npm run buildand `vite.build` will handle the production build. An improved version of the `dev` script will be introduced later in this article.

STEP 2: Configuring vite.config.js

Here is an overview of the Vite configuration file. I will explain the meaning of each line.

import { defineConfig } from "vite";
import liveReload from "vite-plugin-live-reload";
import mkcert from "vite-plugin-mkcert";
import path from "path";

export default defineConfig({
  plugins: [
    liveReload(__dirname   "/**/*.php"),
    mkcert(),
  ],
  root: "",
  base: process.env.NODE_ENV === "development"
    ? "https://localhost:5173/"
    : "/wp-content/themes/your-child-theme/dist/",
  build: {
    outDir: path.resolve(__dirname, "./dist"),
    emptyOutDir: true,
    manifest: true,
    rollupOptions: {
      input: path.resolve(__dirname, "main.js"),
    },
    assetsDir: "",
  },
  server: {
    cors: true,
    strictPort: true,
    port: 5173,
    https: true,
    hmr: {
      host: "localhost",
    },
  },
});

Explanation of each configuration item

ConfigurationValueRole
pluginsliveReload, mkcertFull reload on PHP changes, HTTPS support
baseSwitch between development and productionBase path for assets. Vite server during development, dist directory in production
build.manifesttrueGenerate manifest.json to allow PHP to resolve filenames after building
build.rollupOptions.inputmain.jsEntry point. Includes SCSS imported from here
build.assetsDir“” (empty string)Output files directly to the `dist` directory (do not create subdirectories)
server.httpstrueLaunch an HTTPS development server in conjunction with mkcert
server.hmr.hostlocalhostExplicitly specify the WebSocket connection destination. This may be necessary in Docker environments, etc.

Reasons for requiring HTTPS

If the WordPress site is running over HTTPS (even locally.testif SSL is enabled on the domain), the Vite server must also use HTTPS; otherwise, the connection will be blocked due to mixed content.vite-plugin-mkcertUsing this will automatically generate a self-signed certificate and start an HTTPS development server.

STEP 3: Creating an entry point

main.jsis the Vite entry point. Import your SCSS here.

// main.js
import './vite.scss';

// ここにJavaScriptのコードを書く
document.addEventListener('DOMContentLoaded', () => {
  console.log('Vite HMR is running!');
});

In the SCSS file (vite.scss) contains the child theme’s styles. Thanks to Vite, modifying this SCSS automatically triggers compilation and HMR injection.

STEP 4: Load Assets in functions.php

This is the most important part. We use PHP to switch between loading assets from the Vite development server during development and loading pre-built files via `manifest.json` in the production environment.

Detecting the development environment

First, create a function to check whether the Vite development server is running.

define('VITE_SERVER', 'https://localhost:5173');
define('VITE_ENTRY_POINT', '/main.js');

function is_vite_dev() {
  // 本番環境では常にfalseを返す
  $host = $_SERVER['HTTP_HOST'] ?? '';
  if (strpos($host, 'example.com') !== false) {
    return false;
  }

  // wp-config.phpで定義があれば最優先
  if (defined('IS_VITE_DEVELOPMENT')) {
    return IS_VITE_DEVELOPMENT;
  }

  // .hotファイルの有無で判定
  return file_exists(get_theme_file_path('/.hot'));
}

The key to this check is the .hot file. When the Vite development server starts,.hotand deleted when it exits (see the dev script described later). This ensures that HMR mode is enabled only when the development server is running.

We’ve also added a guard for the production domain. Even if, by some chance,.hotthe file remains on the production server after deployment, the domain check ensures a safe fallback.

Asset Loading Process

add_action('wp_enqueue_scripts', function () {

  if (is_vite_dev()) {
    // 開発環境:Vite HMRクライアント   エントリーポイント
    wp_enqueue_script(
      'vite-client',
      VITE_SERVER . '/@vite/client',
      [], null, true
    );
    wp_enqueue_script(
      'vite-main',
      VITE_SERVER . VITE_ENTRY_POINT,
      [], null, true
    );
  } else {
    // 本番環境:manifest.jsonからビルド済みファイルを読み込み
    $manifest_path = get_theme_file_path('/dist/.vite/manifest.json');
    if (!file_exists($manifest_path)) {
      $manifest_path = get_theme_file_path('/dist/manifest.json');
    }

    if (file_exists($manifest_path)) {
      $manifest = json_decode(
        file_get_contents($manifest_path), true
      );

      // CSSの読み込み
      if (isset($manifest['main.js']['css'])) {
        foreach ($manifest['main.js']['css'] as $css_file) {
          wp_enqueue_style(
            'vite-style',
            get_theme_file_uri('/dist/' . $css_file)
          );
        }
      }

      // JSの読み込み
      if (isset($manifest['main.js']['file'])) {
        wp_enqueue_script(
          'vite-main',
          get_theme_file_uri('/dist/' . $manifest['main.js']['file']),
          [], null, true
        );
      }
    }
  }
}, 100);

In the development environment,@vite/client(the WebSocket client for HMR) and the entry point.@vite/clienthandles communication with the Vite server and injects CSS when changes occur.

Add `type=”module”` to the `script` tag

Since Vite scripts are served in ESM format,'; } return $tag; }, 10, 3);

STEP 5: Start the development server and verify operation

Once all settings are complete, start the development server.

npm run dev

If the Vite logo andhttps://localhost:5173appears in the terminal, it means the setup was successful. When you open the WordPress site in a browser in this state, assets will be served via the Vite development server.

Try changing the color in an SCSS file and saving it. The color in the browser will change instantly without having to reload the page. This is the power of HMR.

When modifying PHP files, a full reload

vite-plugin-live-reloadmonitors changes to PHP files.functions.phpEditing PHP or template files triggers an automatic full reload in the browser. Since the system automatically switches between HMR and full reload, developers don’t need to worry about it.

Production Build Configuration

Once development is complete, build the production files.

npm run build

This willdist/bundled CSS and JS files, along with a `manifest.json` file, are output to the directory.

dist/
├── .vite/
│   └── manifest.json
├── main-CEBPsstE.js
└── main-CmEZvCsu.css

Hashes are appended to filenames for cache busting. Since the hash changes whenever the file content changes, this prevents the issue of old CSS being applied due to browser caching.

Since `manifest.json` is loaded via WordPress’s `functions.php`, no changes to the PHP code are required even if the hash changes. Simply building the project automatically ensures the latest files are referenced.

Dev Script Tips That Make a Difference in Real-World Work

While the basic setup described so far is fully functional, in real-world production environments, accidents like "forgetting to run the build after shutting down the development server, resulting in outdated CSS appearing on the production site" tend to occur.

To prevent this, I’ll introduce a dev script that automatically runs a build when the development server shuts down.

{
  "scripts": {
    "dev": "bash -lc 'touch .hot; trap "rm -f .hot; npm run build; trap - EXIT" INT TERM EXIT; vite'",
    "build": "vite build"
  }
}

Here’s what this script does:

  1. touch .hot — Creates a .hot file. This serves as an indicator that functions.php is in HMR mode.
  2. trap "rm -f .hot; npm run build; ..." INT TERM EXIT — Sets up a trap for Ctrl+C or when the terminal is closed
  3. vite — Starts the development server
  4. When the development server shuts down (Ctrl+C) → Delete the .hot file → Run the build automatically

With this setup,npm run devyou can start development and simply stop with Ctrl+C, ensuring the latest build files are always preserved. Since this eliminates the risk of forgetting to build, I strongly recommend this method.

Example of customizing vite.config.js

High-speed CSS transformation with LightningCSS

In Vite, by replacing the CSS transformer with LightningCSS, you can perform automatic vendor prefixing and nested syntax conversion at native speeds.

// vite.config.js に追加
css: {
  transformer: "lightningcss",
},
npm install --save-dev lightningcss

Using with Tailwind CSS

Vite also works well with Tailwind CSS. In addition to the standard Tailwind setup procedure, simply import a CSS file containing the@tailwindin `main.js`.

Multiple Entry Points

If you want to use different scripts for the frontend and the admin panel,rollupOptions.inputset it to object format.

rollupOptions: {
  input: {
    main: path.resolve(__dirname, "main.js"),
    admin: path.resolve(__dirname, "admin.js"),
  },
},

Common Issues and Solutions

HMR isn't working

  • Check the browser console to see if there are any WebSocket connection errors
  • HTTPS/HTTP Mismatch: If WordPress is running on HTTPS, configure the Vite server to use HTTPS via mkcert
  • Existence of .hot files:is_vite_dev()Check if it is returning true correctly
  • CORS error:vite.config.js'sserver.corsistrueis set

CSS cannot be loaded after building

  • dist/.vite/manifest.json(Vite 5 and later) ordist/manifest.jsonCheck if it exists
  • functions.phpCheck ifis_vite_dev()Check if it returns false (to ensure no .hot files remain)
  • baseCheck if the production path matches the theme directory

CORS error occurs

Check if the WordPress domain (e.g.,my-site.test) and the Vite server (localhost:5173) have different origins, so they are blocked by CORS by default.server.cors: truePlease configure

.gitignore configuration

The following files and directories should be excluded from Git version control.

node_modules/
.hot

dist/Whether to include these in Git depends on your team’s policy. It is common practice to exclude them for CI builds and include them for manual deployments.

Frequently Asked Questions

Can I use this with wp-env or Docker?

Yes, it works. However, to access the Vite server on localhost from WordPress inside Docker,server.hmr.host. Native environments like Herd tend to be simpler to configure.

Can it coexist with jQuery?

Yes, it is possible. Since the jQuery that WordPress loads by default is outside of Vite’s management scope, they can coexist as-is. If you want to manage it with Vite,import $ from 'jquery', you can import it via , but be careful not to cause a double load with WordPress’s jQuery.

Can I use React or Vue.js?

Since Vite is also the official build tool for React and Vue.js, you can certainly use them. Vite is also a strong option when using React for WordPress custom block development. However, since its configuration system differs from WordPress’s standard `@wordpress/scripts`, you’ll need to use them appropriately when developing plugins.

Can I migrate smoothly from an existing Gulp environment?

If your main use cases for Gulp are compiling SCSS/Sass and browser reloading, migrating to Vite is relatively straightforward. Simply replace your `gulpfile.js` configuration with `vite.config.js` and update your setup to import SCSS from `main.js`. Even if you’re using the PostCSS plugin, Vite supports PostCSS natively.

Is Node.js required on the production server?

No. Vite is strictly a development and build tool. All that’s needed for the production environmentdist/the pre-built files in the directory; there is no need to install Node.js.

Summary

Let’s review the steps for integrating Vite into a WordPress theme.

  1. Install Vite and related plugins in the theme directory
  2. Define the entry point, generate the manifest, and configure HTTPS in `vite.config.js`
  3. Import SCSS from `main.js` to create the entry point
  4. Implement development/production switching logic and asset loading in `functions.php`
  5. Launch the development server with `npm run dev` and experience lightning-fast HMR

Once you experience SCSS changes reflecting in the browser the moment you save them, you’ll never want to go back to waiting for Gulp or BrowserSync to reload. With minimal configuration and few files, and easy integration into existing themes, this is a must-try for anyone looking to modernize their WordPress frontend development.

If you want to start by setting up a local environment, check out “Build a WordPress Local Environment in 5 Minutes with Herd and DBngin [Mac].” It explains how to set up a native PHP environment that works well with Vite.

Comment

Copied title and URL