Laravel + Vite: Asset Bundling Fundamentals

+15 Mana ✨

Introduction

Vite is Laravel 13's default frontend build tool, replacing Laravel Mix. It offers a blazing-fast dev server with HMR, native ES modules, and optimized production bundles via esbuild and Rollup.

Key Concepts

  • Vite dev server: A local HTTP server (usually on port 5173) that serves your source files with Hot Module Replacement in development.
  • Entry point: A source file (e.g., resources/js/app.js) declared in vite.config.js and compiled into a bundle.
  • @vite directive: The Blade helper that emits the right <script> and <link> tags for the configured entry points — dev-server URLs in dev, hashed build files in production.
  • Manifest: public/build/manifest.json, emitted by vite build, that maps source entries to their hashed production filenames.

Real World Context

Every Laravel 13 project with CSS or JavaScript goes through Vite. Understanding the dev-server flow, the @vite directive, and the manifest is a prerequisite for any frontend work — from Tailwind to Alpine to full Vue/React SPAs.

Deep Dive

Vite is Laravel's default frontend build tool, replacing Laravel Mix. It offers blazing-fast development with Hot Module Replacement (HMR) and optimized production builds.

Why Vite?

FeatureViteLaravel Mix (Webpack)
Dev Server StartNear-instant (milliseconds)Seconds
Hot ReloadInstantNoticeable lag
Build TimeFast (esbuild)Slower (Webpack)
ConfigurationSimpleComplex
Native ESMYesNo

Project Setup

New Laravel projects already include Vite wired up. Your package.json will look roughly like this — check the exact versions your installer pinned, since they are refreshed with each Laravel release:

json
{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4",
    "laravel-vite-plugin": "^2",
    "tailwindcss": "^4",
    "vite": "^7"
  }
}

Configuration

javascript
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,  // Auto-refresh on Blade changes
        }),
        tailwindcss(),  // Tailwind v4 plugin
    ],
});

The @vite Directive

Include compiled assets in Blade:

blade
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    {{ $slot }}
</body>
</html>

In Development, this generates:

html
<script type="module" src="http://localhost:5173/@vite/client"></script>
<link rel="stylesheet" href="http://localhost:5173/resources/css/app.css">
<script type="module" src="http://localhost:5173/resources/js/app.js"></script>

In Production (after npm run build):

html
<link rel="stylesheet" href="/build/assets/app-BrYLxZ9n.css">
<script type="module" src="/build/assets/app-DfJk2XMz.js"></script>

Development Workflow

bash
# Terminal 1: Start Laravel
php artisan serve

# Terminal 2: Start Vite dev server
npm run dev

Now your browser connects to Laravel at localhost:8000, and Vite serves assets from localhost:5173 with HMR.

CSS with Vite

Tailwind CSS v4 (Default)

Laravel 13 ships with Tailwind v4, which uses a single @import directive — no more @tailwind base/components/utilities, and no tailwind.config.js or postcss.config.js is required:

css
/* resources/css/app.css */
@import "tailwindcss";

/* Custom styles */
.btn-primary {
    @apply px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700;
}

Theme customizations now live in CSS via @theme:

css
@theme {
    --color-brand: oklch(0.72 0.15 250);
    --font-display: "Inter", sans-serif;
}

Importing CSS in JavaScript

javascript
// resources/js/app.js
import '../css/app.css';

// Your JavaScript
console.log('App loaded!');

JavaScript with Vite

ES Modules

javascript
// resources/js/app.js
import './bootstrap';
import { formatDate } from './utils/date';
import Alpine from 'alpinejs';

window.Alpine = Alpine;
Alpine.start();

console.log(formatDate(new Date()));
javascript
// resources/js/utils/date.js
export function formatDate(date) {
    return new Intl.DateTimeFormat('en-US', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
    }).format(date);
}

Installing NPM Packages

bash
npm install alpinejs
npm install axios
npm install lodash-es  # ES module version
javascript
// resources/js/app.js
import Alpine from 'alpinejs';
import axios from 'axios';
import { debounce } from 'lodash-es';

window.Alpine = Alpine;
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

Alpine.start();

Multiple Entry Points

javascript
// vite.config.js
export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
                'resources/css/admin.css',
                'resources/js/admin.js',
            ],
            refresh: true,
        }),
    ],
});
blade
{{-- Main app layout --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])

{{-- Admin layout --}}
@vite(['resources/css/admin.css', 'resources/js/admin.js'])

Production Build

bash
npm run build

This creates optimized assets in public/build/:

  • Minified CSS and JavaScript
  • Content-hashed filenames for cache busting
  • Manifest file for Laravel to resolve paths
public/build/
├── assets/
│   ├── app-BrYLxZ9n.css
│   └── app-DfJk2XMz.js
└── manifest.json

Troubleshooting

blade
{{-- Check if Vite is running --}}
@if (app()->environment('local'))
    <p>Make sure to run: <code>npm run dev</code></p>
@endif

Common issues:

  1. Blank page: Vite dev server not running
  2. CORS errors: Check Vite server URL in config
  3. Assets not found in production: Run npm run build

Common Pitfalls

  1. Running php artisan serve without npm run dev — Vite's dev server isn't running, so @vite renders 404s for every asset.
  2. Forgetting @vite in the layout — No script or stylesheet tags are emitted, and the page renders unstyled.
  3. Hardcoding /resources/... paths in HTML — The source path isn't the production path. Always go through @vite() or Vite::asset().

Best Practices

  1. Always use the @vite directive, not manual <script> tags — Only @vite knows whether to target the dev server or the manifest.
  2. Keep entry points minimal — One CSS entry and one JS entry is usually enough. Split only when you genuinely need separate bundles.
  3. Rely on the dev server for HMR locally — npm run dev + php artisan serve is the standard two-terminal workflow.

Summary

  • Vite replaces Laravel Mix in Laravel 13 and offers HMR, native ESM, and fast builds.
  • vite.config.js declares entry points; @vite([...]) in Blade emits the right tags.
  • Dev uses the Vite dev server (5173); production uses hashed files in public/build/.
  • CSS and JS can be imported with native ES module syntax.
  • npm run dev starts the dev server; npm run build produces the production bundle.
✓ Completed