If you’ve been working with WordPress block themes lately, you probably know the feeling.

You tweak some SCSS.
You save.
You wait.
The page reloads.
Your scroll resets.
Your block preview flickers.

Repeat 100 times a day.

That was me.

I had been using Laravel Mix for years in my WordPress themes. It worked fine — especially for classic themes. But once I started working more deeply with block themes, Full Site Editing, and heavier SCSS structures… my workflow started to feel slow.

Not broken. Just… friction-heavy.

So I decided to rethink my setup.

That’s when I moved to Vite with real HMR.

And it completely changed how development feels.


The Real Problem with My Old Setup

My previous stack looked like this:

  • Laravel Mix
  • Webpack under the hood
  • Sometimes Browsersync for reloads
  • Full page refresh on every change

It wasn’t terrible.

But with block themes:

  • The editor reloads constantly
  • Scroll position resets
  • Inspector panels collapse
  • Block previews flash
  • State disappears

When you’re styling blocks or testing layout changes, those reloads kill your flow.

What I wanted was simple:

👉 Change CSS
👉 See update instantly
👉 No reload
👉 No state loss

That’s exactly what Vite gives you.


Why I Moved from Laravel Mix to Vite

For years, Laravel Mix was my default setup for WordPress themes.

It worked. It was stable. It was familiar.

But over time, a few things started to feel outdated:

  • Slower rebuild times
  • Full page reloads
  • Heavier dependency tree
  • More configuration overhead
  • Webpack complexity I didn’t really need anymore

When I switched to Vite, the difference was immediate:

  • Lightning-fast dev server
  • Native ES modules
  • True HMR (not just reloads)
  • Simpler config
  • Smaller dependency footprint

And honestly, once you experience 50ms CSS updates without reload… there’s no going back.


Why Use Vite HMR in WordPress?

Traditional WordPress workflows (Laravel Mix + Browsersync) usually mean:

  • 2–3 second refresh cycles
  • Scroll position reset
  • Form state lost
  • Flashing page reloads

With Vite HMR:

  • CSS updates instantly
  • No full page reload
  • Scroll stays where it is
  • Forms keep their state
  • Console logs stay intact

In my case, CSS updates went from ~2000ms to ~50ms.

That’s roughly 40x faster feedback.


The Architecture (Simple and Clean)

The setup is intentionally minimal:

  1. Run Vite dev server (port 5173)
  2. Create a small public/hot file when it’s running
  3. WordPress detects that file
  4. Load assets from Vite in development
  5. Load compiled assets in production

No environment toggles.
No manual switching.
No proxy setup.


Step 1 – Configure Vite for WordPress

In vite.config.js:

server: {
  host: 'localhost',
  port: 5173,
  strictPort: true,
  cors: true,
  hmr: {
    host: 'localhost',
    protocol: 'ws',
  },
  watch: {
    usePolling: true,
  },
  origin: 'http://localhost:5173',
}

Why this matters

  • strictPort: true → Prevents unexpected port changes.
  • cors: true → WordPress loads scripts cross-origin.
  • usePolling: true → Critical on Windows/WSL.
  • WebSocket (ws) → Enables real HMR.

Without proper server config, WordPress + Vite can break easily.


Step 2 – The Hot File Strategy

To detect whether Vite is running, I use a small custom plugin that creates:

public/hot

When Vite starts → file is created
When Vite stops → file is removed

WordPress simply checks:

file_exists('/public/hot');

That’s it.

No cURL.
No environment variables.
No proxy detection.

Clean and extremely reliable.


Step 3 – Load Dev Assets with HMR

When the hot file exists, WordPress loads:

<script type="module" crossorigin src="http://localhost:5173/@vite/client"></script>
<script type="module" crossorigin src="http://localhost:5173/resources/scripts/frontend/main.ts"></script>

Important details:

  • @vite/client must load first
  • Scripts must use type="module"
  • Register scripts before declaring dependencies
  • Add crossorigin

If everything works, your console will show:

[vite] connected.

Now when you edit SCSS…

⚡ Instant update. No reload.


Step 4 – Configuring the Vite Helper in WordPress

To make everything clean and reusable, I created a small Vite helper function inside the theme.

Instead of hardcoding dev vs production logic everywhere, I centralize it.

The idea is simple:

  • If public/hot exists → load assets from Vite dev server
  • Otherwise → load compiled files from /public

Here’s a look of the class I’ve created:

final class ViteHelper
{
    private const VITE_DEV_SERVER = 'http://localhost:5173';
    private const VITE_CLIENT = '@vite/client';
    
    /**
     * Verifica si Vite dev server está corriendo
     *
     * @return bool
     */
    public static function is_dev_server_running(): bool
    {
        static $is_running = null;
        
        if ($is_running !== null) {
            return $is_running;
        }
        
        // Método 1: Verificar si el archivo hot existe (Vite lo crea cuando está corriendo)
        $hot_file = get_template_directory() . '/public/hot';
        if (file_exists($hot_file)) {
            $is_running = true;
            return $is_running;
        }
        
        // Método 2: Intentar conexión directa al puerto
        $fp = @fsockopen('localhost', 5173, $errno, $errstr, 1);
        if ($fp) {
            fclose($fp);
            $is_running = true;
            return $is_running;
        }
        
        $is_running = false;
        return $is_running;
    }
    
    /**
     * Encola assets de Vite (dev server con HMR o archivos compilados)
     *
     * @param string $handle Handle único para el asset
     * @param string $entry_path Ruta al entry point (ej: 'resources/scripts/frontend/main.ts')
     * @param array $deps Dependencias
     * @param bool $in_footer Cargar en footer
     * @return void
     */
    public static function enqueue_asset(
        string $handle,
        string $entry_path,
        array $deps = [],
        bool $in_footer = true
    ): void {
        if (self::is_dev_server_running()) {
            // Modo desarrollo: cargar desde Vite dev server con HMR
            self::enqueue_dev_assets($handle, $entry_path, $deps, $in_footer);
        } else {
            // Modo producción: cargar assets compilados
            self::enqueue_prod_assets($handle, $deps, $in_footer);
        }
    }
    
    /**
     * Encola assets desde Vite dev server (desarrollo con HMR)
     *
     * @param string $handle
     * @param string $entry_path
     * @param array $deps
     * @param bool $in_footer
     * @return void
     */
    private static function enqueue_dev_assets(
        string $handle,
        string $entry_path,
        array $deps,
        bool $in_footer
    ): void {
        // Registrar y encolar Vite client para HMR
        wp_register_script(
            'vite-client',
            self::VITE_DEV_SERVER . '/' . self::VITE_CLIENT,
            [],
            null,
            false
        );
        wp_enqueue_script('vite-client');
        
        // Registrar y encolar entry point desde Vite dev server
        wp_register_script(
            $handle,
            self::VITE_DEV_SERVER . '/' . $entry_path,
            ['vite-client'],
            null,
            $in_footer
        );
        wp_enqueue_script($handle);
        
        // Marcar ambos scripts como módulos ES6
        add_filter('script_loader_tag', function ($tag, $script_handle, $src) use ($handle) {
            if ($script_handle === 'vite-client' || $script_handle === $handle) {
                // Agregar type="module" y crossorigin
                $tag = str_replace('<script', '<script type="module" crossorigin', $tag);
            }
            return $tag;
        }, 10, 3);
    }
    
    /**
     * Encola assets compilados (producción)
     *
     * @param string $handle
     * @param array $deps
     * @param bool $in_footer
     * @return void
     */
    private static function enqueue_prod_assets(
        string $handle,
        array $deps,
        bool $in_footer
    ): void {
        $theme_dir = get_stylesheet_directory();
        $theme_uri = get_stylesheet_directory_uri();
        
        // CSS compilado
        $vite_css = $theme_dir . '/public/css/style.css';
        if (file_exists($vite_css)) {
            wp_enqueue_style(
                $handle . '-css',
                $theme_uri . '/public/css/style.css',
                $deps,
                (string) filemtime($vite_css)
            );
        }
        
        // JS compilado
        $vite_js = $theme_dir . '/public/js/main.js';
        if (file_exists($vite_js)) {
            wp_enqueue_script(
                $handle . '-js',
                $theme_uri . '/public/js/main.js',
                $deps,
                (string) filemtime($vite_js),
                $in_footer
            );
        }
    }
}

That’s it.

No conditionals scattered across the theme.
No duplicated logic.
No messy environment flags.

Just one helper that decides everything.

Production Mode

When running:

npm run build

Vite compiles assets into /public.

Then WordPress loads:

  • /public/css/style.css
  • /public/js/main.js

With automatic cache busting:

filemtime($file_path)

Search engines and users see a completely normal WordPress site.

HMR only exists in development.


What’s Next: A WordPress Theme Boilerplate with Vite

After refining this setup across projects, my next step is building a modern WordPress theme boilerplate powered by Vite.

The idea:

  • Clean architecture
  • Built-in HMR
  • Optimized production build
  • Sensible defaults
  • Minimal dependencies
  • Ready for Docker or local environments
  • Developer-first structure

Something lightweight, opinionated, and modern — without unnecessary abstraction.

I’ve used Laravel Mix for years, but moving to Vite feels like the right long-term direction for WordPress theme development.

And the upcoming boilerplate will reflect that.


Final Thoughts

Switching from Laravel Mix to Vite wasn’t just a tooling change.

It improved:

  • Speed
  • Simplicity
  • Developer experience
  • Project maintainability

If you’re building custom WordPress themes in 2026, integrating Vite with HMR is one of the best upgrades you can make.

And if you’re still on Laravel Mix…

It might be time. 🚀