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: truetells Vite to generate a manifest file. -
outDir: 'dist'keeps production assets inside the theme. -
rollupOptions.inputdefines 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 buildsucceeds -
dist/.vite/manifest.jsonexists - 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.
답글 남기기