Back to Blog
Craft CMS Vite Tooling

Setting Up Vite with Craft CMS

· 10 min read

If you're still running Webpack or Laravel Mix on your Craft projects, I get it. They work. But after switching to Vite about two years ago, I honestly can't imagine going back. The dev server starts in milliseconds, hot module replacement actually works the way you'd expect, and the config file is about 10 lines long.

This is the setup I use on every new Craft project now. I'm going to walk through the whole thing from a fresh install so you can follow along, but if you're migrating an existing project, the steps are basically the same. You'll just need to rip out your old build tool first.

What We're Building

By the end of this, you'll have:

  • Vite running as your front-end build tool inside a Craft CMS project
  • Hot module replacement that updates your browser instantly during development
  • A Twig helper that loads the right assets in dev vs. production
  • A production build that outputs hashed, minified files ready for deployment

Here's a quick look at how the pieces fit together:

How Vite + Craft CMS Work Together DEVELOPMENT Browser Vite Dev Server :5173 HMR src/main.js src/style.css Craft CMS Twig templates PRODUCTION Browser dist/assets/ hashed files Craft CMS reads manifest.json manifest.json asset map npm run build

During development, Vite runs its own server on port 5173 and serves your JS and CSS directly to the browser with hot module replacement. Your Twig templates point to Vite's dev server instead of static files. In production, you run npm run build, Vite compiles everything into hashed static files, and Craft reads a manifest.json to know which filenames to use.

Prerequisites

I'm assuming you already have a working Craft CMS installation. If you're starting from scratch, get that set up first. You'll also need Node.js 18+ installed.

The Craft plugin we'll use for this is nystudio107/craft-vite. It's been around for a while and handles all the tricky parts of connecting Vite's output to Twig. If you've used nystudio107's other plugins (SEOmatic, Retcon, ImageOptimize), you know the quality is solid.

Step 1: Install the Dependencies

First, install Vite and Tailwind (or whatever CSS setup you prefer) on the front-end side:

Terminal
npm init -y
npm install -D vite @tailwindcss/vite tailwindcss

Then install the Craft Vite plugin:

Terminal
composer require nystudio107/craft-vite
php craft plugin/install vite

Step 2: Project Structure

Here's the file structure we're going for. Nothing exotic here. The src/ folder holds your front-end source, and Vite will output to web/dist/ so Craft can serve it.

your-craft-project/
├── config/
│ └── vite.php
├── src/
│ ├── js/app.js
│ └── css/app.css
├── templates/
│ └── _layouts/
│ └── base.twig
├── web/
│ └── dist/ ← Vite outputs here
├── vite.config.js
└── package.json

Step 3: Configure Vite

Here's the Vite config. It's short. That's one of the best things about Vite compared to Webpack, where the config file could easily hit 100+ lines.

vite.config.js
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  base: '/dist/',
  build: {
    emptyOutDir: true,
    manifest: true,
    outDir: './web/dist/',
    rollupOptions: {
      input: {
        app: './src/js/app.js',
      },
    },
  },
  plugins: [
    tailwindcss(),
  ],
  server: {
    host: '0.0.0.0',
    port: 5173,
    strictPort: true,
  },
})

A few things to call out:

  • base: '/dist/' tells Vite that all built assets will be served from the /dist/ path on your site
  • manifest: true generates the manifest.json that the Craft plugin reads
  • outDir points to your Craft web root's dist/ folder
  • host: '0.0.0.0' is important if you're running Craft inside Docker or DDEV, since the browser needs to reach Vite from outside the container
If you're using DDEV, you'll also need to expose port 5173. Add web_extra_exposed_ports: [{name: "vite", container_port: 5173, http_port: 5172, https_port: 5173}] to your .ddev/config.yaml.

Step 4: Configure the Craft Plugin

Create a config file at config/vite.php. This tells the plugin where to find Vite's dev server and built assets:

config/vite.php
<?php

use craft\helpers\App;

return [
    'useDevServer' => App::env('VITE_DEV_SERVER') === 'true',
    'devServerPublic' => 'http://localhost:5173',
    'serverPublic' => '/dist/',
    'manifestPath' => '@webroot/dist/.vite/manifest.json',
    'errorEntry' => 'src/js/app.js',
    'cacheKeySuffix' => '',
];

Then add this to your .env file:

.env
# Set to "true" for local development, remove or set "false" for production
VITE_DEV_SERVER=true
Don't forget to set VITE_DEV_SERVER=false (or just remove it) in your production environment. If the plugin tries to reach the dev server in production, your pages will hang while it times out.

Step 5: Set Up Your Entry Files

Create your main JavaScript entry point. This is where you import your CSS too, which is how Vite knows to process it:

src/js/app.js
import '../css/app.css'

// Your JS goes here
console.log('Vite + Craft CMS is running')

And your CSS file:

src/css/app.css
@import "tailwindcss";

/* Your custom styles */
body {
  font-family: system-ui, sans-serif;
}

Step 6: Wire It Up in Twig

This is where it all comes together. In your base Twig layout, use the plugin's craft.vite.script() function to load assets. It handles all the dev vs. production logic for you:

templates/_layouts/base.twig
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ siteName }}</title>

    {{ craft.vite.script("src/js/app.js") }}
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>

That single craft.vite.script() call does a lot of heavy lifting. In development, it injects:

  • A connection to Vite's HMR websocket
  • Your entry script as an ES module pointing to localhost:5173

In production, it reads the manifest and outputs the hashed filenames with proper <link> and <script> tags.

Step 7: Add Your npm Scripts

Update your package.json with these scripts:

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

Running It

Open two terminal tabs. In the first one, start Craft like you normally would (however your local environment runs). In the second:

Terminal
npm run dev

That's it. Open your site in the browser and you should see everything working. Try changing a CSS value and watch it update instantly without a full page reload. That's HMR doing its thing.

When you're ready to deploy:

Terminal
npm run build

This generates your compiled assets in web/dist/ along with the manifest file. Commit the dist/ folder (or run the build as part of your deploy pipeline, which is what I'd recommend).

I prefer running npm run build as part of my deployment process rather than committing the dist/ folder. It keeps the repo cleaner and means you never have merge conflicts on compiled assets. If you're using Forge, you can add it as a deploy script. For GitHub Actions, just add a build step before the deploy.

Common Gotchas

HMR not working behind DDEV/Docker

If hot reloading doesn't work, it's almost always a port issue. Make sure port 5173 is exposed from your container and that devServerPublic in your Vite config matches the URL your browser can actually reach. If you're using DDEV with HTTPS, the dev server URL needs to use HTTPS too.

Assets not loading in production

Check three things: Did you run npm run build? Is VITE_DEV_SERVER set to false (or unset) in your production .env? Does the manifestPath in config/vite.php point to the right location? The manifest path changed in newer versions of Vite, so it's now at .vite/manifest.json inside the output directory instead of just manifest.json.

Fonts and images not resolving

If you reference static assets like fonts or images in your CSS, make sure they're either in the public/ folder (served as-is) or imported through JavaScript/CSS so Vite can process them. Files in public/ won't get hashed filenames, but they'll be copied to the build output.

Going Further

Once you have this foundation in place, there's a lot you can layer on top:

  • Multiple entry points if you need different JS/CSS bundles for different sections of the site
  • PostCSS plugins like autoprefixer (Vite supports PostCSS out of the box, just add a postcss.config.js)
  • Legacy browser support via the @vitejs/plugin-legacy package if you need to support older browsers
  • Image optimization with vite-plugin-image-optimizer

But honestly, the basic setup I've outlined here covers 90% of what most Craft projects need. Start with this, and add complexity only when you actually need it.


I've been running this exact stack on every Craft project for the past couple of years and it's been rock solid. If you run into any issues getting it set up or have questions about more advanced configurations, feel free to reach out.