Nitro
New Nitro 2.x: browser-first components for Laravel

Write PHP.
It runs in the browser.

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.

Get started GitHub
$ composer require nitro/nitro
blog.test/posts
  • Hello, Nitro Sara · 3 min
  • Helpers every Laravel app needs Omar · 6 min
  • Writing forms that validate as you type Lena · 8 min
  • Uploads, previews and progress Omar · 5 min
  • Pages that change without a reload Sara · 4 min
  • When a method needs the server Lena · 7 min
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')]

Browser + server

One component, two places

A PHP class and a Blade view. The server renders the first page with Blade; the browser runs the same component, compiled.

Browser

Methods run in the browser

Clicks, typing, filtering and sorting answer at once: your methods are JavaScript now, with PHP's rules kept.

Server

#[Server] when it matters

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

Try it right here

Each example below is a Nitro component, running on this page.

Forms

Browser + server

Laravel's validation, as you know it

The 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>
Live

Uploads

Browser + server

Files, previewed at once

Bind 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
Live

Navigation

Browser

Pages without a reload

nitro: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

See exactly what runs where

The class and the view are yours. Open Compiled to read the JavaScript Nitro makes of them: the same component, the same rules.

Browser + server
<?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]);
    }
}
Live
Not saved yet

And the rest of an app

Build your first component in five minutes

Install the package, write a class and a view, and watch it run in the browser.