Tables
A table is a listing backed by a data source. You declare columns in PHP; Lattice renders the
React table and handles sorting, filtering, and pagination, fetching rows from the table’s
endpoint. Nothing about a table is tied to a database — a table is a
TableDefinition plus a source that turns a request into rows. An
Eloquent source ships out of the box, and an in-memory array, an HTTP API,
or a search index works just as well.
#[AsTable('app.products')]class ProductsTable extends TableDefinition{ public function columns(): array { return [ TextColumn::make('name')->sortable()->filterable(), NumberColumn::make('price')->sortable()->filterable(), BooleanColumn::make('featured'), TextColumn::make('updated_at')->label('Updated')->dateTime()->sortable(), ]; }
public function source(): TableSource { return new CallbackTableSource( fn (TableQuery $query) => TableResult::fromItems($this->products()), ); }
}- actionsLabel:"Actions"
- bulkActions:[]
- data:[{"featured":true,"name":"Desk Lamp","price":"49.00","updated_at":"2026-05-30 09:15:00"},{"featured":false,"name":"Office Chair","price":"189.00","updated_at":"2026-06-02 14:40:00"},{"featured":true,"name":"Monitor Stand","price":"75.50","updated_at":"2026-06-08 08:05:00"}]
- emptyLabel:"No results"
- endpoint:null
- filters:[]
- layout:null
- lazy:false
- pagination:{"currentPage":null,"from":1,"hasMore":false,"lastPage":null,"mode":"none","nextPage":null,"perPage":null,"to":3,"total":3}
- resizableColumns:false
- resizeIndicator:false
- state:{"filters":[],"page":1,"perPage":25,"sorts":[],"tableFilterIndicators":[],"tableFilters":{}}
- striped:true
- align:"start"
- badge:null
- copyable:false
- date:null
- hiddenByDefault:false
- label:"Name"
- link:null
- multiple:null
- sortable:true
- toggleable:false
- width:"md"
- align:"end"
- compact:false
- hiddenByDefault:false
- label:"Price"
- maximumFractionDigits:null
- minimumFractionDigits:null
- sortable:true
- toggleable:false
- unit:null
- width:"md"
- align:"start"
- filter:null
- hiddenByDefault:false
- label:"Featured"
- sortable:false
- toggleable:false
- width:"md"
- align:"start"
- badge:null
- copyable:false
- date:{"dateStyle":"medium","timeStyle":"medium"}
- filter:null
- hiddenByDefault:false
- label:"Updated"
- link:null
- multiple:null
- sortable:true
- toggleable:false
- width:"md"
How a table loads data
Section titled “How a table loads data”Every sort, filter, and page change is a fresh GET to the table’s endpoint. The request becomes a
TableQuery that the data source turns into rows — so the React table never holds the full dataset, it
asks for the page it needs:
Defining a table
Section titled “Defining a table”Extend TableDefinition and implement its two abstract methods — columns() and source(). The
#[AsTable] attribute gives the table a stable id so it can be discovered and addressed by its own
endpoint.
use Lattice\Lattice\Attributes\AsTable;use Lattice\Lattice\Tables\CallbackTableSource;use Lattice\Lattice\Tables\Columns\NumberColumn;use Lattice\Lattice\Tables\Columns\TextColumn;use Lattice\Lattice\Tables\Contracts\TableSource;use Lattice\Lattice\Tables\TableDefinition;use Lattice\Lattice\Tables\TableQuery;use Lattice\Lattice\Tables\TableResult;
#[AsTable('app.products')]class ProductsTable extends TableDefinition{ public function columns(): array { return [ TextColumn::make('name')->sortable()->filterable(), NumberColumn::make('price')->sortable(), ]; }
public function source(): TableSource { return new CallbackTableSource( fn (TableQuery $query) => TableResult::fromItems($this->rows()), ); }}Everything except the source is declared the same way whatever backs the table. TableDefinition
exposes these hooks beyond columns() and source():
| Method | Purpose |
|---|---|
columns() |
The columns to render — see Columns. |
source() |
The data source that turns a query into rows. |
filters() |
Table-level dedicated filters. |
perPage() |
Rows per page (default 25). |
pagination() |
The pagination mode. |
striped() |
Alternate row shading. |
resizableColumns() |
Let users drag column borders to resize. Widths persist per table in the browser. |
resizeIndicator() |
Show a drag indicator on column borders when resizing is enabled. |
emptyLabel() |
Text shown when the table has no rows (default “No results”). |
actionsLabel() |
Accessible label for the row-actions column (default “Actions”). |
actions($row) |
Per-row actions. |
bulkActions() |
Bulk actions for selected rows. |
Each hook is a method you override. For example, to enable resizable columns with a drag indicator:
public function resizableColumns(): bool{ return true;}
public function resizeIndicator(): bool{ return true;}Resized widths are remembered per table in the browser, with a control to reset them to the defaults.
Rendering a table
Section titled “Rendering a table”Render a table on a page with Table::use(), passing the definition class:
use Lattice\Lattice\Tables\Components\Table;
Table::use(ProductsTable::class);Table::lazy() renders the same table but defers the first data load until the component mounts —
useful inside a tab or a section that isn’t visible on page load.
Next steps
Section titled “Next steps”- Data sources — the
TableSourcecontract and backing a table with anything. - Eloquent tables — the database-backed source,
builder(), and relation columns. - Columns — the column types and their display options.
- Filtering — column filters and dedicated table filters.
- Sorting & pagination — sortable columns and the pagination modes.
- Actions — row and bulk actions.