Nitro compiles your Laravel components, PHP classes and Blade views, to JavaScript.
Methods run instantly in the browser, with no request. Mark one #[Server] when it needs the server.
class PostSearch extends Component
{
#[Url(as: 'q')]
public string $search = '';
public array $posts = [];
public function mount(): void
{
$this->posts = Post::latest()->get()->toArray();
}
#[Computed]
public function found(): array
{
return array_filter($this->posts, fn ($post) => stripos($post['title'], $this->search) !== false);
}
}
<div>
<input nitro:model="search" placeholder="Search posts...">
<ul>
@foreach ($this->found as $post)
<li>{{ $post['title'] }}</li>
@endforeach
</ul>
</div>
Filtered in the browser, with no request
Kept in the URL by #[Url(as: 'q')]
A PHP class and a Blade view. The server renders the first page with Blade; the browser runs the same component, compiled.
Clicks, typing, filtering and sorting answer at once: your methods are JavaScript now, with PHP's rules kept.
The database, mail, payments. One attribute, and the call carries the signed state to the server and back.
Everything an app needs, the Laravel way
Each example below is a Nitro component, running on this page.
Forms
Browser + serverThe rules sit on the property. The server checks them before your method runs, and the message
lands in @error. nitro:loading covers the wait. Try a wrong address, then a right one.
#[Validate('required|email')]
public string $email = '';
#[Server]
public function subscribe(): void
{
$this->validate();
Subscriber::create(['email' => $this->email]);
}
<form nitro:submit="subscribe">
<input nitro:model="email" type="email">
@error('email') <p>{{ $message }}</p> @enderror
<button nitro:loading.attr="disabled">Subscribe</button>
</form>
Uploads
Browser + serverBind a file input like any other. The browser shows the preview before anything is saved; the upload, the rules and the storage stay on the server.
public ?Upload $photo = null;
#[Server]
public function save(): void
{
$this->validate(['photo' => 'required|image|max:2048']);
$this->photo->store('photos', 'public');
}
<input type="file" nitro:model="photo">
@if ($photo)
<img src="{{ $photo->temporaryUrl() }}">
@endif
Navigation
Browsernitro:navigate swaps the page in place and keeps back and forward working.
nitro:prefetch loads it when the pointer comes near, so the click is instant.
<a nitro:navigate href="/posts">Posts</a>
<a nitro:navigate nitro:prefetch href="/posts/hello-nitro">
Hello, Nitro
</a>
No magic, just a compiler
The class and the view are yours. Open Compiled to read the JavaScript Nitro makes of them: the same component, the same rules.
<?php
namespace App\Nitro\Components;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Attributes\Validate;
use Nitro\Component;
class Counter extends Component
{
#[Validate('integer|min:0|max:1000')]
public int $count = 0;
#[Locked]
public string $savedAt = '';
public function mount(): void
{
$this->count = session('counter.count', 0);
$this->savedAt = session('counter.saved_at', '');
}
public function increment(): void
{
$this->count++;
}
#[Server]
public function save(): void
{
$this->savedAt = now()->format('H:i:s');
// This demo keeps the count in the session. Your app would save it to the database.
session(['counter.count' => $this->count, 'counter.saved_at' => $this->savedAt]);
}
}
<div class="flex flex-wrap items-center gap-3">
<button nitro:click="increment" class="rounded-lg bg-slate-900 px-3.5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200">
Count: {{ $count }}
</button>
<button nitro:click="save" nitro:loading.attr="disabled" class="rounded-lg px-3.5 py-2 text-sm font-semibold text-slate-700 ring-1 ring-slate-300 hover:bg-slate-50 disabled:opacity-50 dark:text-slate-200 dark:ring-white/15 dark:hover:bg-white/5">
Save on the server
</button>
<span class="text-sm text-slate-500">{{ $savedAt === '' ? 'Not saved yet' : 'Saved at '.$savedAt.'. Reload the page and it stays.' }}</span>
@error('count')
<span class="text-sm text-red-600 dark:text-red-400">{{ $message }}</span>
@enderror
</div>
Compiling…
Route::nitro() pages with nested layouts, middleware and route model binding.
BrowserState shared between components: a cart badge and its drawer, kept in step.
Browser + serverTitles, descriptions and meta tags per page, updated as you navigate.
ServerLocked properties, typed updates, the page's middleware checked again on every call.
BrowserEnter and leave transitions, and parts of a page that start only when needed.
BrowserWrite the view in Blade, or in Vue, React, Solid or Svelte: the same PHP class behind it.
Install the package, write a class and a view, and watch it run in the browser.