Lattice: Describe Inertia UIs in PHP

Last updated on by

Lattice: Describe Inertia UIs in PHP image

Lattice is a server-driven UI framework for Laravel that lets you describe a screen — its pages, forms, tables, actions, and menus — in PHP and have it render as real React components over Inertia. Lattice makes the server the single source of truth for what a screen is, leaving the client with one job — rendering it.

A page is a PHP class that builds a tree of component definitions. Lattice serializes that tree to a typed payload, ships it over Inertia as a normal page visit, and a single React component resolves each node against a component registry to draw it.

What you get out of that model:

  • Pages as classes, routed automatically. A #[AsPage] attribute registers the route; Lattice scans your configured paths and wires it up without a manual route entry.
  • Forms backed by Laravel validation. Fields are declared in PHP, validated with standard Laravel rules, and optionally validated live through Precognition.
  • Eloquent-backed tables. Columns, sorting, filtering, and pagination are defined on a table class that returns a query builder.
  • Server-side actions that return effects. An action runs your PHP on click and hands back instructions — a toast, a redirect, a component refresh — for the client to dispatch.

Pages

A page extends the base Page class, carries an #[AsPage] attribute for its route, and builds its UI in render(). The component tree is assembled with fluent PHP — Stack, Grid, Heading, Card, and so on — rather than JSX:

use Lattice\Lattice\Attributes\AsPage;
use Lattice\Lattice\Core\Components\Card;
use Lattice\Lattice\Core\Components\Grid;
use Lattice\Lattice\Core\Components\Heading;
use Lattice\Lattice\Core\Components\Stack;
use Lattice\Lattice\Core\Components\Text;
use Lattice\Lattice\Core\Enums\Gap;
use Lattice\Lattice\Core\PageSchema;
use Lattice\Lattice\Http\Page as BasePage;
 
#[AsPage(route: '/dashboard', middleware: ['web'])]
final class DashboardPage extends BasePage
{
public function title(): string
{
return 'Dashboard';
}
 
public function render(PageSchema $schema): PageSchema
{
return $schema->schema([
Stack::make('dashboard')
->gap(Gap::Large)
->schema([
Heading::make('Dashboard'),
Text::make('Everything below is described in PHP and rendered as React.'),
Grid::make('stats')
->columns(2)
->schema([
Card::make('Orders', '128 this week.'),
Card::make('Revenue', '$4,210 this week.'),
]),
]),
]);
}
}

Route parameters resolve straight into the render() signature with Laravel's route-model binding, and an authorize() method gates access before the page renders:

#[AsPage(route: '/products/{product}/edit')]
class ProductEditPage extends Page
{
public function authorize(Request $request): bool
{
return $request->user()?->can('update', Product::class) ?? false;
}
 
public function render(PageSchema $schema, Product $product): PageSchema
{
return $schema->schema([
Heading::make("Edit {$product->name}"),
]);
}
}

Forms

A form is a class extending FormDefinition that declares its fields and handles the submission. Lattice renders the React inputs, validates the request against your Laravel rules, and runs handle() on a successful submit:

use Illuminate\Http\Request;
use Lattice\Lattice\Attributes\AsForm;
use Lattice\Lattice\Forms\Components\Form as FormComponent;
use Lattice\Lattice\Forms\Components\TextInput;
use Lattice\Lattice\Forms\FormDefinition;
use Symfony\Component\HttpFoundation\Response;
 
#[AsForm('app.profile.form')]
class ProfileForm extends FormDefinition
{
public function definition(FormComponent $form, Request $request): FormComponent
{
return $form->schema([
TextInput::make('name', 'Name')->rules(['required', 'string', 'max:255']),
TextInput::make('email', 'Email')->email()->rules(['required', 'email']),
]);
}
 
public function handle(Request $request): Response
{
$validated = $this->validate($request);
$request->user()->update($validated);
 
return redirect('/profile');
}
}

Dropping the form onto a page is a single call, with fluent configuration for the HTTP method, submit label, and the data it's filled with. Adding ->precognitive(500) opts into live validation through Laravel Precognition:

Form::use(ProfileForm::class)
->method(HttpMethod::Patch)
->submitLabel('Save changes')
->precognitive(500)
->fill([
'name' => $user->name,
'email' => $user->email,
]);

Tables

Tables extend EloquentTableDefinition. You declare the columns and return a query builder; sorting, filtering, and pagination are handled for you based on which columns you mark as sortable() or filterable():

use Illuminate\Database\Eloquent\Builder;
use Lattice\Lattice\Attributes\AsTable;
use Lattice\Lattice\Tables\Columns\BooleanColumn;
use Lattice\Lattice\Tables\Columns\NumberColumn;
use Lattice\Lattice\Tables\Columns\TextColumn;
use Lattice\Lattice\Tables\EloquentTableDefinition;
use Lattice\Lattice\Tables\TableQuery;
 
#[AsTable('app.products')]
class ProductsTable extends EloquentTableDefinition
{
public function columns(): array
{
return [
TextColumn::make('name')->sortable()->filterable(),
NumberColumn::make('price')->sortable()->filterable(),
BooleanColumn::make('featured'),
TextColumn::make('updated_at')->date('Y-m-d')->sortable(),
];
}
 
public function builder(TableQuery $query): Builder
{
return Product::query();
}
}

Rendering it on a page reuses the same ::use() pattern as forms, so a products page composes its heading and table from the two classes:

$schema->schema([
Heading::make('Products'),
Table::use(ProductsTable::class),
]);

Actions and client effects

Actions are where the round trip shows up. An action extends ActionDefinition, describes its button in definition(), and runs server-side in handle(). Rather than returning a view, it returns an ActionResult carrying effects — instructions the client dispatches, such as a toast and a component reload:

use Illuminate\Http\Request;
use Lattice\Lattice\Actions\ActionDefinition;
use Lattice\Lattice\Actions\ActionResult;
use Lattice\Lattice\Actions\Components\Action;
use Lattice\Lattice\Attributes\AsAction;
use Lattice\Lattice\Core\Enums\ButtonVariant;
use Lattice\Lattice\Core\Enums\Variant;
 
#[AsAction('app.products.archive')]
class ArchiveProductAction extends ActionDefinition
{
public function definition(Action $action): Action
{
return $action
->label('Archive')
->variant(ButtonVariant::Destructive)
->confirm('Archive product?', 'This hides it from the catalogue.');
}
 
public function handle(Request $request): ActionResult
{
$product = $this->product($request);
$product->update(['status' => 'archived']);
 
return ActionResult::success()
->toast(Variant::Success, 'Product archived.')
->reloadComponent('app.products');
}
}

Attached to a table row, the action carries the row's context to the server so handle() knows which record it's acting on:

Action::use(ArchiveProductAction::class)
->context(['product_id' => $row['id']]);

Learn More

📕 Installation, component reference, and theming options are found in the documentation.

💻 The source is available on GitHub.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech
Laravel Cloud logo

Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Cloud
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $9500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm
Acquaint Softtech logo

Acquaint Softtech

Acquaint Softtech offers AI-ready Laravel developers who onboard in 48 hours at $3000/Month with no lengthy sales process and a 100 percent money-back guarantee.

Acquaint Softtech
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit

The latest

View all →
Ship AI with Laravel: Give Your AI Agent Live Web Search image

Ship AI with Laravel: Give Your AI Agent Live Web Search

Read article
How We Cached Laravel News at the Edge with Fast Laravel image

How We Cached Laravel News at the Edge with Fast Laravel

Read article
The artisan dev Command in Laravel 13.16.0 image

The artisan dev Command in Laravel 13.16.0

Read article
LaraOwl: Self-Hosted Monitoring for Laravel Applications image

LaraOwl: Self-Hosted Monitoring for Laravel Applications

Read article
Filament Storage Monitor: Track Disk Usage From Your Filament Dashboard image

Filament Storage Monitor: Track Disk Usage From Your Filament Dashboard

Read article
Subscriptionify: Feature-Based Subscription Management for Laravel image

Subscriptionify: Feature-Based Subscription Management for Laravel

Read article