Tree Table
CommunityTurn any Filament table into an expandable parent/child tree — sub-rows render inline, search & filter aware, with expand/collapse all.
filament/
namespace. Review the source and install at your own risk. Found
malware or an unresolved security issue the author won't
address?
Report it
.
Author:
Kiyan Doguc
Documentation
Expandable parent/child tree rows for Filament v4 & v5 tables. Show only top-level parents, expand them with a chevron, and render the sub-rows inline as real table rows — search and filter stay correct, and you get expand-all / collapse-all buttons.
- ✅ Inline expandable rows that keep all your existing columns
- ✅ Search / filter aware — flatten to a flat match list, or keep the tree and reveal each match with its ancestor path (auto-expanded, non-matching ancestors dimmed as context)
- ✅ Custom sibling ordering —
defaultSort()plus full->sortable(query: ...)/ relationship-column support inside the tree - ✅ Clear sub-rows without forcing an icon convention: a corner-arrow glyph on children and/or a coloured accent bar (+ optional per-depth tint) — mix or switch, fully themeable
- ✅ Expand-all / collapse-all header actions
- ✅ Optional pagination by root — keep each family on one page instead of splitting parents and children across page boundaries (
paginateByRoot()) - ✅ Database-agnostic ordering (Postgres / MySQL / SQLite)
- ✅ No stored
level/depth column required — depth is derived fromparent_id
#Requirements
- PHP 8.2+
- Filament v4 or v5
#Installation
composer require kisame76/filament-tree-table
The CSS auto-registers with Filament. After install (and on deploy) run:
php artisan filament:assets
Optionally publish the config:
php artisan vendor:publish --tag="filament-tree-table-config"
#Usage
Your model needs a self-referencing parent_id column and a children() HasMany
relationship (both names are configurable).
It takes two pieces — applying the tree where the table is defined, and opting the page in.
#1. Apply the tree in your table definition
In Filament's resource structure the table lives in Tables/<Name>Table::configure()
(or directly in the resource's table() method). Wrap it with ExpandableRows:
use Filament\Tables\Table;
use Kisame76\FilamentTreeTable\ExpandableRows;
class CategoriesTable
{
public static function configure(Table $table): Table
{
return ExpandableRows::make()
->parentKey('parent_id') // default
->childrenRelationship('children') // default
->applyTo(
$table->columns([
// ... your normal columns
])
);
}
}
applyTo() prepends the chevron toggle column, wires the tree query, applies the
per-row styling, and adds the expand/collapse-all header actions.
#2. Opt the page in
The List page is the Livewire component that holds the expand state, so add the
interface + trait there. It has no table() of its own — that stays in the table class:
use Filament\Resources\Pages\ListRecords;
use Kisame76\FilamentTreeTable\Concerns\InteractsWithExpandableRows;
use Kisame76\FilamentTreeTable\Contracts\HasExpandableRows;
class ListCategories extends ListRecords implements HasExpandableRows
{
use InteractsWithExpandableRows;
protected static string $resource = CategoryResource::class;
}
Relation managers and table widgets define
table()on the component itself, so there is only one class: add theExpandableRows::make()->applyTo(...)call and theimplements HasExpandableRows+use InteractsWithExpandableRowsto that same class.
#Configuration
Every cue is an independent toggle — combine or switch freely:
ExpandableRows::make()
->parentKey('parent_id') // default
->childrenRelationship('children') // default
->recordKey(fn ($record) => $record->getKey()) // for non-default primary keys
->grid(true) // per-level stepping (indentation) on/off
->cornerArrow(true) // corner-down-right glyph on children
->accentBar(true) // coloured bar on the child's left edge
->depthTint(true) // per-depth background tint (child rows lighter)
->recordClasses(fn ($record, int $depth) => []) // extend/override row classes
->expandAllAction(true)
->collapseAllAction(true)
->flattenOnSort(false) // false (default): sort hierarchically, keep tree; true: flat sorted list
->flattenOnFilter(true) // true (default): a filter flattens the tree; false: keep the tree + reveal matches with ancestors
->flattenOnSearch(true) // true (default): a search flattens the tree; false: keep the tree + reveal matches with ancestors
->paginateByRoot(false) // false (default): paginate by row; true: paginate by root so families never split across pages
->defaultSort('sort') // default sibling order: a column name...
->defaultSort(fn ($query, $direction) => $query->orderBy('sort', $direction)) // ...or a closure (order by a related/computed value)
->applyTo($table);
Project-wide defaults live in config/filament-tree-table.php.
#Translations
The expand-all / collapse-all action labels are translatable. English (en) and German
(de) ship with the package; publish the language files to add or override locales:
php artisan vendor:publish --tag="filament-tree-table-translations"
This writes lang/vendor/filament-tree-table/{locale}/tree-table.php, where each locale
defines actions.expand_all and actions.collapse_all.
#Theming
All visuals are driven by CSS variables — override them in your panel theme:
.ftt-row {
--ftt-slot: 1.5rem; /* width of each marker column (indent step) */
--ftt-accent-color: rgb(99 102 241 / 0.85);
--ftt-tint-color: rgb(99 102 241);
}
#How it works / caveats
- Filtering / search: by default (
flattenOnFilter(true)/flattenOnSearch(true)) an active filter or search drops the tree and shows a flat list of every match, so nothing stays hidden behind a collapsed parent. Set either tofalseto keep the tree — matches are then shown together with their ancestor path (auto-expanded), and the non-matching ancestors are dimmed via the.ftt-contextclass (style it in your theme). While a filter/search drives the expansion the chevrons are non-interactive and the expand/collapse-all actions hide, so the displayed state can't be toggled out of sync. - Pagination: by default the page counts visible tree rows, so page sizes shift as
you expand and a branch can span a page boundary (a parent on one page, its children on
the next). Enable
->paginateByRoot()to paginate by root instead: each page holds N roots (the per-page selection) plus all of their currently visible descendants, so a family is never split — the row count per page then varies, and "Showing X to Y of Z" and the page count refer to roots. It is a no-op while the view is flattened (sort/filter/search), under a non-default pagination mode, or with->paginated(false). For very deep trees you can also disable pagination entirely with->paginated(false). - Sorting: a column sort is delegated to the column itself, so
->sortable(query: ...)closures and relationship/computed columns order the tree exactly as they would a flat table. UsedefaultSort()for the sibling order when no column sort is active. WithflattenOnSort(false)(default) the sort stays hierarchical — each sibling group follows the column while keeping the tree grouped under its parent (Jira-style); setflattenOnSort(true)to drop the tree and show a flat sorted list. - Stepping:
grid(false)removes the per-level indentation (flat rows); the hierarchy is then shown only byaccentBar/depthTint.gridandcornerArroware independent. - Column manager: the chevron toggle column is kept out of the column-manager panel
(the
toggleableColumns()/reorderableColumns()dropdown) and is always pinned as the first column, so a persisted column order can never hide it or push it to the back. It still carries a non-breaking-space label internally (->toggleColumnLabel('…')to change it) because Filament rejects blank labels on reorderable columns — the label is no longer shown anywhere. The pinning is done byInteractsWithExpandableRows, which overrides Filament'sgetDefaultTableColumnState()/updateTableColumns(). On a List page, relation manager, or table widget this just works (the base class suppliesInteractsWithTable). The only exception is a bare custom Livewire component that usesInteractsWithTableandInteractsWithExpandableRowsside by side — there, resolve the trait conflict explicitly:use InteractsWithTable, InteractsWithExpandableRows { InteractsWithExpandableRows::getDefaultTableColumnState insteadof InteractsWithTable; InteractsWithExpandableRows::updateTableColumns insteadof InteractsWithTable; InteractsWithExpandableRows::paginateTableQuery insteadof InteractsWithTable; }. - Components that do not implement
HasExpandableRows(e.g. a widget sharing the sametable()definition) render completely flat — every wired behaviour self-disables.
#License
MIT.
The author
From the same author
Featured Plugins
A selection of plugins curated by the Filament team
Custom Dashboards
Let your users build and share their own dashboards with a drag-and-drop interface. Define your data sources in PHP and let them do the rest.
Filament
Blueprint
Filament Blueprint is a premium Laravel Boost extension that helps AI agents produce accurate, detailed implementation plans and security reports for Filament apps.
Filament
Spotlight Pro
Browse your Filament Panel with ease. Filament Spotlight Pro adds a Spotlight/Raycast like Command Palette to your Filament Panel.
Dennis Koch