If you’ve been building Laravel apps and dreading the moment someone says “make it dynamic” — because that usually means wiring up Vue, React, or vanilla JS — then Laravel Volt with Livewire 3 is about to become your new best friend. In this post, we’re going full dev-to-dev: no fluff, just what Livewire 3 and Volt actually are, how they work together, and how to build a real live search feature without touching a single line of JavaScript.
Let’s get into it.
What Is Livewire 3 — And What Changed from v2?
If you haven’t used Livewire before, here’s the quick version: Livewire is a full-stack framework built on top of Laravel that lets you build reactive, interactive UIs using pure PHP. No API endpoints. No JavaScript frameworks. Just PHP classes and Blade templates.
When a user clicks a button or types in a search box, Livewire sends a lightweight AJAX request to the server, re-renders the component, and surgically updates the DOM. You get the feel of a JavaScript SPA, built entirely in PHP.
Livewire 3 — released in 2023 — was a ground-up rewrite. Here’s what’s different from v2:
- Better performance — the network request overhead got significantly reduced. Components feel snappier.
- Automatic script injection — you no longer need to manually include
@livewireScriptsin most setups. wire:navigate— SPA-like page navigation out of the box. Click a link, get a smooth transition. No full page reload.- Loose coupling with Alpine.js — Alpine is now bundled with Livewire, so you get JavaScript interactivity for the small things (toggling a dropdown, animation) without adding a separate dependency.
- Improved lazy loading — components can be deferred with
wire:lazy, so your page loads first and the component renders after. - New
wire:model.live— in v2 you usedwire:model.debounce.500ms. In v3, it’s cleaner:wire:model.livefor instant reactivity orwire:model.blurto update on field blur. - Single-file components via Volt — this is the big one, and the main reason we’re here today.
Livewire v2 is still out there but it’s in maintenance mode. If you’re starting a new project, v3 is the way to go.
What Is Laravel Volt — And Why Is It Different?
Here’s the thing with traditional Livewire (even v3): when you create a component, you get two files.
app/Livewire/UserSearch.php ← the PHP class
resources/views/livewire/user-search.blade.php ← the Blade templateFor a small app with 5–10 components, that’s fine. For a real project with 40+ components, you end up constantly jumping between two files every time you need to change something. It’s a friction point.
Laravel Volt solves this. Volt is an elegantly crafted functional API for Livewire that supports single-file components — your PHP logic and your Blade template live in the same .blade.php file. Think of it like Vue’s single-file components (.vue files), but you’re writing PHP the whole time.
Behind the scenes, Volt compiles your single-file component into a standard Livewire class. You write less boilerplate, jump between fewer files, and keep your component logic and view tightly together where it belongs.
Volt was introduced at Laracon US 2023 and has been getting huge traction ever since — especially among developers who prefer staying in PHP-land.
Here’s a quick visual comparison:
Traditional Livewire Component (2 files):
// app/Livewire/Counter.php
class Counter extends Component
{
public int $count = 0;
public function increment(): void
{
$this->count++;
}
public function render(): View
{
return view('livewire.counter');
}
}{{-- resources/views/livewire/counter.blade.php --}}
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+1</button>
</div>Volt Component (1 file):
{{-- resources/views/livewire/counter.blade.php --}}
<strong><?php</strong>
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
<strong>?></strong>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+1</button>
</div>Same result. Half the files. Everything in one place.
Install and Setup — Step by Step
Let’s get a fresh Laravel project up and running with Livewire 3 and Volt.
Step 1: Create a Laravel Project
composer create-project laravel/laravel volt-demo
cd volt-demoStep 2: Install Livewire 3
composer require livewire/livewireLivewire 3 will be pulled in automatically. No extra config needed for basic setup.
Step 3: Install Laravel Volt
composer require livewire/voltThen run the install artisan command:
php artisan volt:installThis does two things:
- Creates
app/Providers/VoltServiceProvider.php - Registers it in your
bootstrap/providers.php(orconfig/app.phpdepending on your Laravel version)
The VoltServiceProvider tells Volt where to look for your single-file components. By default, it scans:
resources/views/livewireresources/views/pages
You can customize this in the provider’s boot() method if needed.
Step 4: Set Up Your Layout
Livewire needs a base layout file. Create one at resources/views/components/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Volt Demo</title>
@livewireStyles
</head>
<body>
{{ $slot }}
@livewireScripts
</body>
</html>That’s your base layout. Livewire will inject its scripts and styles automatically.
Build a Simple Counter Component with Volt
Before we tackle the real-world example, let’s wire up a simple counter so you can see Volt’s structure clearly.
Generate the Component
php artisan make:volt counterThis creates resources/views/livewire/counter.blade.php. Open it and replace the contents with:
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
$decrement = fn () => $this->count--;
$reset = fn () => $this->count = 0;
<strong>?></strong>
<div class="p-6 text-center">
<h2 class="text-4xl font-bold mb-4">{{ $count }}</h2>
<div class="flex gap-3 justify-center">
<button wire:click="decrement" class="px-4 py-2 bg-red-500 text-white rounded">−</button>
<button wire:click="reset" class="px-4 py-2 bg-gray-400 text-white rounded">Reset</button>
<button wire:click="increment" class="px-4 py-2 bg-green-500 text-white rounded">+</button>
</div>
</div>Register a Route
In routes/web.php:
use Livewire\Volt\Volt;
Volt::route('/counter', 'counter');Visit /counter and you’ve got a fully reactive counter — increment, decrement, reset — with zero JavaScript written by you.
A few things to note about the Volt syntax here:
state()— defines the reactive properties (equivalent topublic $count = 0in a class).$increment = fn () => ...— defines a method. The variable name becomes the method name Livewire calls when you usewire:click="increment".- No
render()method needed — Volt handles that automatically.
Real-World Example — Live Search Users Without Page Reload
This is where things get genuinely useful. Let’s build a live user search: type in a box, see results filter in real time, no page reload, no JavaScript.
Step 1: Set Up Your Database
Make sure you’ve got a users table (it comes with Laravel by default). Seed it with some fake users:
php artisan tinker\App\Models\User::factory(50)->create();Now you’ve got 50 fake users to search through.
Step 2: Create the Volt Component
php artisan make:volt user-searchOpen resources/views/livewire/user-search.blade.php and build it out:
use App\Models\User;
use function Livewire\Volt\{state, computed};
state(['search' => '']);
$users = computed(function () {
return User::query()
->when($this->search, function ($query) {
$query->where('name', 'like', '%' . $this->search . '%')
->orWhere('email', 'like', '%' . $this->search . '%');
})
->orderBy('name')
->limit(20)
->get();
});
<strong>?></strong>
<div class="max-w-2xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-4">Search Users</h1>
<!-- Search Input -->
<input
type="text"
wire:model.live.debounce.300ms="search"
placeholder="Type a name or email..."
class="w-full border border-gray-300 rounded px-4 py-2 mb-6 focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
<!-- Results -->
@if($this->users->isEmpty())
<p class="text-gray-500">No users found for "{{ $search }}".</p>
@else
<p class="text-sm text-gray-400 mb-3">Showing {{ $this->users->count() }} result(s)</p>
<ul class="divide-y divide-gray-200">
@foreach($this->users as $user)
<li class="py-3 flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold text-sm">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<div>
<p class="font-medium text-gray-800">{{ $user->name }}</p>
<p class="text-sm text-gray-500">{{ $user->email }}</p>
</div>
</li>
@endforeach
</ul>
@endif
</div>Step 3: Add the Route
// routes/web.php
use Livewire\Volt\Volt;
Volt::route('/users/search', 'user-search');Step 4: Visit It
Start your server:
php artisan serveGo to http://localhost:8000/users/search and start typing. Users filter in real time as you type — no JavaScript, no API endpoint, no Vue, no React.
Let’s break down what’s happening here:
wire:model.live.debounce.300ms="search" — this binds the input to the $search state property. .live means it updates on every keystroke. .debounce.300ms means it waits 300 milliseconds after you stop typing before firing the server request. This prevents a request on every single keypress.
computed() — this is a Volt function that creates a computed property (like a getter). The $users property is re-evaluated every time $search changes. Livewire is smart about this — it only re-runs the query when the dependency actually changes.
$this->users — in the Blade template, you access computed properties via $this->propertyName.
That’s it. The whole thing — logic, query, and UI — lives in one file. Under 60 lines.
Volt’s Functional API — A Quick Reference
Here are the main Volt functions you’ll use day-to-day:
| Function | What it does |
|---|---|
state(['key' => 'default']) | Defines reactive properties |
computed(fn) | Creates a computed/derived property |
mount(fn) | Runs on component initialization |
rules([...]) | Defines validation rules |
layout('view.name') | Sets a custom layout for full-page components |
title('Page Title') | Sets the browser tab title |
protect(fn) | Marks a method as server-only (not callable from frontend) |
on(['event' => fn]) | Listens for Livewire events |
A quick example using mount and rules together:
use function Livewire\Volt\{state, rules, mount};
state(['name' => '', 'email' => '']);
rules([
'name' => 'required|min:3',
'email' => 'required|email',
]);
mount(function () {
// Runs once when the component initializes
// Great for setting up initial state from props
});
$submit = function () {
$data = $this->validate();
// Save to DB, fire event, redirect, etc.
$this->reset('name', 'email');
session()->flash('message', 'Saved!');
};
<strong>?></strong>
<div>
@if(session('message'))
<p class="text-green-600">{{ session('message') }}</p>
@endif
<input wire:model="name" type="text" placeholder="Name" />
@error('name') <span class="text-red-500">{{ $message }}</span> @enderror
<input wire:model="email" type="email" placeholder="Email" />
@error('email') <span class="text-red-500">{{ $message }}</span> @enderror
<button wire:click="submit">Save</button>
</div>Clean. Declarative. No extra PHP file needed.
Class-Based Volt — When You Need More Structure
Volt’s functional API is great for most components, but if you’re coming from v2 or just prefer classes, Volt also supports a class-based single-file format:
use Livewire\Volt\Component;
new class extends Component {
public string $search = '';
public function getResultsProperty()
{
return \App\Models\User::where('name', 'like', "%{$this->search}%")->get();
}
}
<strong>?></strong>
<div>
<input wire:model.live="search" type="text" placeholder="Search..." />
@foreach($this->results as $user)
<p>{{ $user->name }}</p>
@endforeach
</div>Same single file, but the PHP block looks like a standard Livewire class. This is great if you want full IDE support, type hints, and are migrating an existing Livewire v2 project.
To generate a class-based Volt component:
php artisan make:volt user-list --classShould You Use Volt or Traditional Livewire?
Here’s the honest take:
Use Volt when:
- You’re building a new project from scratch
- Your components are small to medium sized
- You want less file switching in your editor
- You like the Vue/React single-file component mental model
- You’re solo or on a small team
Stick with traditional Livewire (separate files) when:
- You have a large team where some devs only touch Blade and others only touch PHP
- Your component logic is very heavy (200+ lines of PHP)
- You need complex unit testing on the component class specifically
- You’re on Livewire v2 and not ready to migrate yet
There’s no wrong answer here. Volt doesn’t replace Livewire — it’s a layer on top of it. You can even mix both styles in the same project.
Wrapping Up
Here’s what we covered:
- Livewire 3 is a ground-up rewrite with better performance, cleaner APIs, SPA navigation, and Alpine.js bundled in.
- Laravel Volt brings single-file components to Livewire — your PHP logic and Blade template in one
.blade.phpfile. - Setup is quick:
composer require livewire/volt, thenphp artisan volt:install. - The functional API (
state,computed,rules,mount) gives you a clean, declarative way to write components. - A live user search — real-time filtering, no JavaScript — is genuinely just one file and about 50 lines of code.
If you haven’t tried Volt yet, start with one small component in your next project. The “one file” mental model clicks fast, and once it does, going back to two files feels unnecessarily clunky.
For the full official docs, head to https://livewire.laravel.com/docs/3.x/volt. Everything Volt can do is documented there, including advanced topics like testing Volt components, using Volt with Folio (Laravel’s file-based routing), and rendering components as full pages.
Happy building. ⚡


Leave a Reply