DEV Community

Cover image for How to Build a Vite-Powered WordPress Theme
Amr Osama
Amr Osama

Posted on

How to Build a Vite-Powered WordPress Theme

Modern WordPress theme development does not have to feel stuck in the past.
You can still use the parts of WordPress that are excellent - templates, hooks, custom fields, routing, editorial workflows - while giving your theme a modern frontend build setup with Vite.

In this tutorial, we will build a Vite-powered WordPress theme setup that gives you:

  • Sass support
  • JavaScript modules
  • fast local development
  • hashed production assets
  • a Vite manifest
  • WordPress-safe asset enqueueing
  • graceful fallbacks when the production build is missing

The examples are based on a custom portfolio theme, but the structure works for most classic WordPress themes.

The Goal

We want a theme structure like this:

your-theme/
  assets/
  dist/
  inc/
    enqueue.php
  src/
    js/
      main.js
    scss/
      main.scss
  functions.php
  style.css
  vite.config.js
  package.json
Enter fullscreen mode Exit fullscreen mode

During development, we write code in src/.

For production, Vite builds optimized files into dist/, and WordPress loads the correct hashed filenames from Vite's manifest.

1. Create a Basic Theme

At minimum, a classic WordPress theme needs:

style.css
functions.php
index.php
Enter fullscreen mode Exit fullscreen mode

Your style.css should include the theme header:

/*
Theme Name: Your Theme
Author: Your Name
Version: 1.0.0
*/
Enter fullscreen mode Exit fullscreen mode

Even if Vite handles your real CSS, WordPress still uses this file to identify the theme.

2. Install Vite and Sass

Inside your theme folder, initialize npm if you have not already:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Then install Vite and Sass:

npm install -D vite sass
Enter fullscreen mode Exit fullscreen mode

If your theme uses frontend libraries, install them normally:

npm install gsap lenis
Enter fullscreen mode Exit fullscreen mode

Your scripts can be simple:

{
  "scripts": {
    "dev": "vite --host 127.0.0.1",
    "build": "vite build"
  },
  "devDependencies": {
    "sass": "^1.89.0",
    "vite": "^5.4.19"
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Create Your Frontend Entrypoint

Create src/js/main.js:

import '../scss/main.scss';

console.log('Theme scripts loaded');
Enter fullscreen mode Exit fullscreen mode

Then create src/scss/main.scss:

:root {
  --color-bg: #101012;
  --color-text: #f5f5f5;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: system-ui, sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

The important detail is that your JavaScript entry imports your Sass file. Vite will follow that import and generate the final CSS asset during the build.

4. Configure Vite for WordPress

Create vite.config.js in the theme root:

import { defineConfig } from 'vite';
import { resolve } from 'node:path';

export default defineConfig({
  root: '.',
  base: './',
  build: {
    manifest: true,
    emptyOutDir: true,
    outDir: 'dist',
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'src/js/main.js')
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

The key pieces:

  • manifest: true tells Vite to generate a manifest file.
  • outDir: 'dist' keeps production assets inside the theme.
  • rollupOptions.input defines the source entrypoint.
  • base: './' helps generated asset references stay relative.

After running a production build, Vite creates files like:

dist/
  assets/
    main-B8x7k2.css
    main-D9s1a4.js
  .vite/
    manifest.json
Enter fullscreen mode Exit fullscreen mode

The hashed filenames are great for caching, but WordPress needs a way to discover them. That is where the manifest comes in.

5. Load the Manifest in PHP

In functions.php, define a few theme constants:

<?php
defined( 'ABSPATH' ) || exit;

define( 'THEME_DIR', get_template_directory() );
define( 'THEME_URI', get_template_directory_uri() );
define( 'THEME_VER', '1.0.0' );

require_once THEME_DIR . '/inc/enqueue.php';
Enter fullscreen mode Exit fullscreen mode

Then create inc/enqueue.php:

<?php
defined( 'ABSPATH' ) || exit;

function theme_vite_manifest() {
    $path = THEME_DIR . '/dist/.vite/manifest.json';

    if ( ! file_exists( $path ) ) {
        return array();
    }

    $manifest = json_decode( (string) file_get_contents( $path ), true );

    return is_array( $manifest ) ? $manifest : array();
}
Enter fullscreen mode Exit fullscreen mode

This helper reads the manifest only if it exists. If the build folder is missing, it returns an empty array instead of breaking the site.

6. Enqueue the Built Assets

Now we can enqueue the generated CSS and JS:

function theme_enqueue_assets() {
    $manifest = theme_vite_manifest();
    $entry    = isset( $manifest['src/js/main.js'] ) ? $manifest['src/js/main.js'] : null;

    if ( $entry && ! empty( $entry['css'][0] ) ) {
        wp_enqueue_style(
            'theme-app',
            THEME_URI . '/dist/' . $entry['css'][0],
            array(),
            THEME_VER
        );
    } else {
        wp_enqueue_style(
            'theme-style',
            get_stylesheet_uri(),
            array(),
            THEME_VER
        );
    }

    if ( $entry && ! empty( $entry['file'] ) ) {
        wp_enqueue_script(
            'theme-app',
            THEME_URI . '/dist/' . $entry['file'],
            array(),
            THEME_VER,
            true
        );

        wp_script_add_data( 'theme-app', 'type', 'module' );
    }
}
add_action( 'wp_enqueue_scripts', 'theme_enqueue_assets' );
Enter fullscreen mode Exit fullscreen mode

This does three useful things:

  • loads the hashed production CSS file from Vite
  • loads the hashed production JS file from Vite
  • adds type="module" to the script tag

It also falls back to style.css if the production CSS is not available.

7. Run the Build

Now run:

npm run build
Enter fullscreen mode Exit fullscreen mode

If everything is wired correctly, Vite should create your dist/ folder and the manifest file.

Then refresh WordPress and inspect the page source. You should see something like:

<link rel="stylesheet" href="/wp-content/themes/your-theme/dist/assets/main-B8x7k2.css">
<script type="module" src="/wp-content/themes/your-theme/dist/assets/main-D9s1a4.js"></script>
Enter fullscreen mode Exit fullscreen mode

That means WordPress is loading your compiled Vite assets.

8. Optional: Add a Development Fallback

For a simple theme, you can build before refreshing WordPress.

But if you want a smoother local workflow, you can also detect the Vite dev server and load assets from it during development.

A minimal version looks like this:

function theme_is_vite_dev_server_running() {
    $response = wp_remote_get( 'http://127.0.0.1:5173/src/js/main.js', array(
        'timeout' => 1,
    ) );

    return ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response );
}
Enter fullscreen mode Exit fullscreen mode

Then inside your enqueue function:

if ( theme_is_vite_dev_server_running() ) {
    wp_enqueue_script(
        'vite-client',
        'http://127.0.0.1:5173/@vite/client',
        array(),
        null,
        true
    );

    wp_script_add_data( 'vite-client', 'type', 'module' );

    wp_enqueue_script(
        'theme-app',
        'http://127.0.0.1:5173/src/js/main.js',
        array(),
        null,
        true
    );

    wp_script_add_data( 'theme-app', 'type', 'module' );

    return;
}
Enter fullscreen mode Exit fullscreen mode

Now you can run:

npm run dev
Enter fullscreen mode Exit fullscreen mode

and let Vite serve your source modules locally.

For production, always run:

npm run build
Enter fullscreen mode Exit fullscreen mode

and let WordPress load from dist/.

9. Optional: Inline Critical CSS

For performance-focused themes, you can go one step further and extract a small critical CSS file during the Vite build.

Here is a simplified Vite plugin example:

import { defineConfig } from 'vite';
import { resolve } from 'node:path';
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs';

export default defineConfig({
  build: {
    manifest: true,
    outDir: 'dist',
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'src/js/main.js')
      }
    }
  },
  plugins: [
    {
      name: 'theme-critical-css',
      closeBundle() {
        const manifestPath = resolve(__dirname, 'dist/.vite/manifest.json');

        if (!existsSync(manifestPath)) return;

        const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
        const entry = manifest['src/js/main.js'];

        if (!entry?.css?.[0]) return;

        const css = readFileSync(resolve(__dirname, 'dist', entry.css[0]), 'utf8');
        const critical = css.slice(0, 7000);

        mkdirSync(resolve(__dirname, 'dist'), { recursive: true });
        writeFileSync(resolve(__dirname, 'dist/critical.css'), critical);
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Then inline it in WordPress:

add_action( 'wp_head', function () {
    $critical = THEME_DIR . '/dist/critical.css';

    if ( file_exists( $critical ) ) {
        echo '<style id="theme-critical-css">' . wp_strip_all_tags( (string) file_get_contents( $critical ) ) . '</style>';
    }
}, 1 );
Enter fullscreen mode Exit fullscreen mode

This is intentionally simple. For a serious production site, use a more deliberate critical CSS strategy instead of slicing the first few kilobytes blindly.

Common Gotchas

The manifest path is wrong

Vite 5 writes the manifest to:

dist/.vite/manifest.json
Enter fullscreen mode Exit fullscreen mode

Make sure your PHP code reads from that path.

WordPress is loading old files

Clear any caching plugin, CDN cache, and browser cache. Hashed files help, but WordPress or your host may still cache HTML.

Your script is not treated as a module

Add this after wp_enqueue_script():

wp_script_add_data( 'theme-app', 'type', 'module' );
Enter fullscreen mode Exit fullscreen mode

Your theme breaks before the first build

Always guard manifest loading with file_exists(). A missing dist/ folder should not white-screen the site.

Final Checklist

Before shipping:

  • npm run build succeeds
  • dist/.vite/manifest.json exists
  • WordPress loads CSS from dist/assets
  • WordPress loads JS from dist/assets
  • the script tag has type="module"
  • the site still works if dist/ is missing
  • production caches are cleared after deployment

Wrap-Up

This setup gives WordPress a modern frontend workflow without fighting the platform.

WordPress still handles templates, routing, admin content, and PHP hooks. Vite handles Sass, JavaScript modules, bundling, and hashed production assets.

That split is the sweet spot: WordPress where WordPress is strong, Vite where the frontend needs speed.

About me

I am Amr Osama, a senior web developer building fast, SEO-ready WordPress and React platforms. I share practical notes from real projects on custom WordPress development, performance, technical SEO, and modern frontend workflows.

Top comments (0)