June 15, 2026

Introducing Lattice: server-driven React for Laravel, without the coupling

By Manuel Christlieb — Staff Engineer

Part 1 of the Building Lattice series.

I have a new side project: Lattice. This is the first post in a series where I build it in public — the decisions, the trade-offs, and the things I get wrong along the way. Let me start with why it exists.

The itch

I really like Filament. Describing an admin panel in PHP — forms, tables, actions — instead of hand-wiring a frontend is a genuinely great developer experience. But two things kept nagging at me.

First, I wanted a real React frontend, not Blade and Livewire. Plenty of the apps I work on already have a React front end over Inertia, and I didn’t want to give that up to get the PHP-first ergonomics.

Second, and more importantly: most server-driven builders buy their convenience by coupling tightly to the data model. You point them at an Eloquent model and they infer the UI from it. That’s magic until you want something the model doesn’t describe, and then you’re fighting the abstraction. I wanted to know whether you could keep the PHP-first DX while staying decoupled from the data model and unopinionated about how you build. Lattice is, in part, me trying to prove that you can.

The thesis

You describe your interface — pages, forms, tables, actions, menus — in PHP on the server, and Lattice renders it with real React components on the client over Inertia. There’s no hand-wired API and no UI contract duplicated across two languages. You keep building the way you already do in Laravel; your users get a polished React front end.

The part I care about most: nothing is derived from your database schema unless you choose to. A form is the fields you put on it. A table is the columns you define. Persistence is code you write. The UI and the data model are two separate things that you wire together on purpose.

What it looks like

Here’s a page. It’s a plain PHP class — no route file entry, no controller, and no Inertia page component to write by hand:

<?php

namespace App\Pages;

use Lattice\Lattice\Attributes\Page;
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;

#[Page(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.'),
                        ]),
                ]),
        ]);
    }
}

Lattice scans your app, finds every class with a #[Page] attribute, and registers the route automatically. Visit /dashboard and it renders through Inertia as React. That’s the whole loop: PHP in, React out, nothing wired by hand in between.

Forms work the same way. A form is a definition with the fields you choose and a handle() method you write — it isn’t bound to a model’s columns:

$form->schema([
    Card::make('Group')->schema([
        TextInput::make('name', 'Name')->rules(['required', 'string', 'max:255']),
    ]),
]);

You decide what the field is, what it validates, and what happens on submit. The model never forced the shape of the UI on you. Tables and actions follow the same pattern — which is exactly the decoupling I was after.

It’s early, and that’s the point

Lattice is young. The API will move, there are rough edges, and I’m sure some of the decisions I’m confident about today will look naive in six months. That’s the whole reason I’m writing this series instead of waiting for a 1.0 — building in public means I get to think out loud and you get to tell me where I’m wrong.

If you want to look around: the code and docs are at latticephp.com, and it’s on Packagist as lattice-php/lattice and npm as @lattice-php/lattice.

Next up, I’ll dig into the core idea underneath all of this: how PHP describes a UI as a tree of nodes and how the React side renders it — the contract that makes the decoupling work.