livewire/volt
Volt adds a functional, single-file component API to Livewire, letting PHP logic and Blade templates live in one file. Write cleaner components with less boilerplate.
composer require livewire/volt
php artisan volt:install # scaffolds resources/views/livewire
php artisan make:volt counter # functional SFC (default)
php artisan make:volt counter --class # class-based SFC
Where to look first:
resources/views/livewire/counter.blade.php — it shows the whole file anatomy (PHP block on top, Blade template below, optional @script / @assets).Livewire\Volt\Volt facade — Volt::route(), Volt::test(), Volt::actingAs().livewire.laravel.com/docs/volt (the README is a stub and points there).The smallest first use case: take an interactive bit of an existing Blade page (a search box, a filter panel, a “load more” list) and replace the surrounding controller/partial with one Volt file, then drop <livewire:search-box /> into the existing view. No route, no controller, no service provider registration.
First full-page use case: a component becomes the route itself.
use Livewire\Volt\Volt;
Volt::route('/posts', 'posts.index')->name('posts.index');
Volt::route('/posts/{post}', 'posts.show')->middleware('auth');
If you already have Livewire in the app, that’s the entire setup — Volt is a compiler that turns SFCs into ordinary Livewire components at runtime, so everything you know about wire: directives, validation, and livewire/update applies unchanged.
Functional API with named actions and computed properties. Names come from the variable you assign, not a string argument:
<?php
use App\Models\Post;
use function Livewire\Volt\{state, computed, action, mount};
state(['search' => ''])->url();
mount(function (Post $post) {
// route model binding and container-resolved DI both work here
$this->authorize('view', $post);
});
$results = computed(fn () => Post::where('title', 'like', "%{$this->search}%")->limit(10)->get());
$clear = action(function () {
$this->search = '';
});
?>
<div>
<input wire:model.live.debounce.300ms="search" type="search">
<button wire:click="clear">Clear</button>
@foreach ($results as $post)
<div wire:key="post-{{ $post->id }}">{{ $post->title }}</div>
@endforeach
</div>
Page chrome in the same file. layout('components.layouts.app'), title('Posts') (or title(fn () => $post->title) when it depends on loaded data). No ->layout() on the route, no extra view files.
Query-string state instead of manual plumbing. state(['page' => 1, 'tag' => null])->url() puts the values in the URL; withQueryParams(['tag' => null]) seeds defaults from the request to make bookmarkable filters; ->except([...]) / ->session() control what is persisted where. This replaces most of the #[Url] boilerplate you would write in class components.
Class API inside an SFC when you outgrow functions. Same file format, real class with inheritance, interfaces, and constructor-less DI:
<?php
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public function with(): array
{
return ['posts' => Post::paginate(15)];
}
}; ?>
<div>{{ $posts->links() }}</div>
Rule of thumb: functional API for presentation + light state; class SFC the moment you need a trait, an interface, a base class, with() for shareable view data, or heavy type-hinting. uses(WithPagination::class) exists in the functional API too, but the class form is clearer once there are three of them.
Mix modes in one app. Keep heavy/legacy components as class components in app/Livewire and write new interactive leaf components as Volt files. Volt detects class-based projects and resolves names accordingly, so <livewire:old-thing /> and <livewire:new-thing /> coexist. This is the pragmatic migration path: rewrite a page only when you are already editing it.
Reuse existing Blade with @volt. Inline one-off components directly in a Blade view without creating a file — good for prototypes and for feature flags you aren’t ready to promote to a file yet.
Colocate JS. @script / @assets at the bottom of the SFC keep Alpine/Chart.js/Datepicker wiring next to the markup that uses it. Keep the template to a single root element so script placement stays predictable.
Testing. Volt::test('counter')->assertSee(0)->call('increment')->assertSee(1); in Feature/Pest tests, Volt::actingAs($user) for auth-dependent components, and $this->get('/')->assertSeeVolt('counter') / assertDontSeeVolt(...) when asserting on an HTTP response. Named actions from the functional API are callable by their variable name, exactly like class methods.
state(), computed(), action(), rules(), layout(), title(), uses() are read by the precompiler from the file’s top-level statements. Wrap them in a function or an if and they silently stop being registered — or blow up with a confusing compile error.resources/views/livewire/ files breaks <livewire:... /> references and Volt::route() strings with a ComponentNotFoundException that names the old component. Dotted names (make:volt posts.index) map to nested directories — support for the dot was added in v1.7.2, so don’t rely on it on very old installs.livewire.view_path, not a Volt-specific config key. If components “don’t exist” after you restructure directories, check Livewire’s published config before anything else.mount() ($this->authorize(...)) rather than assuming “no route means nobody can hit it.”route:cache segfaults, Blade::getPath() returning null during view caching, opcache_invalidate missing, and multiple-script/root-element issues. When behavior makes no sense — 404s for a file that clearly exists, stale output after edits — run php artisan optimize:clear and confirm you are on the latest 1.x. Keep Volt and Livewire upgraded together; recent releases are largely compatibility fixes for Livewire 4, PHP 8.5, and Laravel 13.storage/framework/views; open the compiled file to see exactly what your closure compiled into. It is the fastest way to understand why $this behaves unexpectedly inside an action.new an SFC class yourself — go through Volt::test() / Livewire::test(). That constraint is the main argument for keeping domain logic in services, actions, or plain classes and leaving the Volt file as glue; those are testable in isolation and survive a rewrite to class components.rules() accepts an array or a closure (useful when rules depend on $this state). Errors surface normally through $errors, and validate() inside an action behaves like Livewire. Don’t hand-roll validation in the Blade template.make:volt --class) rather than fighting the detection.How can I help you explore Laravel packages today?