PHPackages                             solution-forest/filament-nestable-tree - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. solution-forest/filament-nestable-tree

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

solution-forest/filament-nestable-tree
======================================

A nestable drag-and-drop tree component for Filament v4 and v5. Supports Eloquent models (including kalnoy/nestedset), static record arrays, per-node actions, multi-tree pages, cross-tree drag-and-drop, lazy loading, and async child loading.

1.0.2(1mo ago)4317—1.8%MITPHP ^8.2

Since May 19Compare

[ Source](https://github.com/solutionforest/filament-nestable-tree)[ Packagist](https://packagist.org/packages/solution-forest/filament-nestable-tree)[ Docs](https://github.com/solutionforest/filament-nestable-tree)[ RSS](/packages/solution-forest-filament-nestable-tree/feed)WikiDiscussions Synced 3w ago

READMEChangelog (3)Dependencies (14)Versions (5)Used By (0)

Filament Nestable Tree
======================

[](#filament-nestable-tree)

[![Latest Version on Packagist](https://camo.githubusercontent.com/cd43252c725749c2b84259056ab5ad1c024d3103b62c778967d8a91ee3375c44/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f736f6c7574696f6e2d666f726573742f66696c616d656e742d6e65737461626c652d747265652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/solution-forest/filament-nestable-tree)[![GitHub Tests Action Status](https://camo.githubusercontent.com/46ce2e946bd1e156b38bb093e1d355da7a8e0a1cfc0cbedf8e947064e454ddb5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f736f6c7574696f6e666f726573742f66696c616d656e742d6e65737461626c652d747265652f72756e2d74657374732e796d6c3f6272616e63683d312e78266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/solutionforest/filament-nestable-tree/actions?query=workflow%3Arun-tests+branch%3A1.x)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/5e7ff081b5f41dcd5faafcfaf528fe1702aa07b9a557822f793b14693e399c0e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f736f6c7574696f6e666f726573742f66696c616d656e742d6e65737461626c652d747265652f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d312e78266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/solutionforest/filament-nestable-tree/actions?query=workflow%3A%22Fix+PHP+code+styling%22+branch%3A1.x)[![Total Downloads](https://camo.githubusercontent.com/08a072e335f5032e4e331c094d400f689a6e568d94f8a4b480bd5018d40f96a3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f736f6c7574696f6e2d666f726573742f66696c616d656e742d6e65737461626c652d747265652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/solution-forest/filament-nestable-tree)

A nestable drag-and-drop tree component for [Filament](https://filamentphp.com) v4 and v5. Supports Eloquent models (including [`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset)), static record arrays, per-node actions, multi-tree pages, cross-tree drag-and-drop, lazy loading, and async child loading.

> **Example usage** — see the [demo application](https://filament-cms-website-demo.solutionforest.net/admin) and the ready-to-run [fixture pages](tests/fixtures/Pages) in this repository.

[![Basic Tree](./assets/01-basic-tree.png)](./assets/01-basic-tree.png)

---

Which Package Should I Use?
---------------------------

[](#which-package-should-i-use)

- Need a simple tree solution with quick setup? Use [filament-tree](https://github.com/solutionforest/filament-tree).
- Need to handle heavy-load menus or large, complex trees? Use this package (`filament-nestable-tree`).

---

Installation
------------

[](#installation)

```
composer require solution-forest/filament-nestable-tree
```

Important

If you are using Filament Panels with a custom theme, add the plugin's views to your theme CSS file so Tailwind can scan them:

```
@source '../../../../vendor/solution-forest/filament-nestable-tree/resources/**/*.blade.php';
```

If you have not yet set up a custom theme, follow the [Filament theming guide](https://filamentphp.com/docs/4.x/styling/overview#creating-a-custom-theme) first.

---

Quick Start
-----------

[](#quick-start)

### 1 — Standalone Tree Page

[](#1--standalone-tree-page)

Create a Filament page that shows a tree:

```
php artisan make:filament-tree-page CategoryTreePage
```

```
use SolutionForest\FilamentNestableTree\Filament\Pages\TreePage;
use SolutionForest\FilamentNestableTree\Tree;

class CategoryTreePage extends TreePage
{
    protected static ?string $navigationLabel = 'Categories';

    public function tree(Tree $tree): Tree
    {
        return $tree->model(Category::class)->labelField('title');
    }
}
```

### 2 — Resource Tree Page (replaces ListRecords)

[](#2--resource-tree-page-replaces-listrecords)

```
php artisan make:filament-tree-resource-page ManageCategoryTree
```

```
use SolutionForest\FilamentNestableTree\Filament\Resources\Pages\TreePage;
use SolutionForest\FilamentNestableTree\Tree;

class ManageCategoryTree extends TreePage
{
    public static string $resource = CategoryResource::class;

    public function tree(Tree $tree): Tree
    {
        return parent::tree($tree)   // includes default EditAction + DeleteAction
            ->model(Category::class)
            ->labelField('title');
    }
}
```

Register the page in your resource's `getPages()`:

```
public static function getPages(): array
{
    return [
        'index' => ManageCategoryTree::route('/'),
    ];
}
```

### 3 — Tree Widget

[](#3--tree-widget)

```
php artisan make:filament-tree-widget CategoryTreeWidget
```

```
use SolutionForest\FilamentNestableTree\Filament\Widgets\Tree as TreeWidget;
use SolutionForest\FilamentNestableTree\Tree;

class CategoryTreeWidget extends TreeWidget
{
    public function tree(Tree $tree): Tree
    {
        return $tree->model(Category::class)->labelField('title');
    }
}
```

### 4 — Embed in Any Livewire Component (InteractsWithTree)

[](#4--embed-in-any-livewire-component-interactswithtree)

Add the `InteractsWithTree` trait to any Livewire component (including custom pages, widgets, or plain Livewire components) to embed a tree without extending a base class:

```
use Livewire\Component;
use SolutionForest\FilamentNestableTree\Concerns\InteractsWithTree;
use SolutionForest\FilamentNestableTree\Tree;

class MyCustomPage extends Component
{
    use InteractsWithTree;

    public function tree(Tree $tree): Tree
    {
        return $tree->model(Category::class)->labelField('title');
    }
}
```

Then render the tree in your Blade view:

```
@include('filament-nestable-tree::livewire.components.tree', [
    'wireNodesProperty'  => 'treeNodes',
    'treeKeyName'        => null,
    'treeConfig'         => $this->getCachedTree(),
    'isSearchable'       => $this->getCachedTree()->isSearchable(),
    'allowDragDrop'      => $this->getCachedTree()->isDraggable(),
    'allowCrossCategory' => $this->getCachedTree()->isCrossCategoryAllowed(),
    'toolbarActions'     => $this->getCachedTree()->getToolbarActions(),
    'lazy'               => $this->getCachedTree()->isLazy(),
    'hasNodeActions'     => ! empty($this->getCachedTree()->getNodeActions()),
])
```

> Livewire automatically calls `mountInteractsWithTree()` after your component's `mount()` to populate the tree nodes — no manual setup required.

---

Tree Configuration Reference
----------------------------

[](#tree-configuration-reference)

All options are fluent methods on the `Tree` instance returned from `tree()` or `trees()`.

MethodDefaultDescription`->model(Category::class)``null`Eloquent model to load the tree from`->records([...])``[]`Static nested/flat array (alternative to `model`)`->labelField('name')``'name'`Attribute used as the display label`->recordKeyField('id')``'id'`Attribute used as the unique identifier`->parentKeyField('parent_id')``'parent_id'`Attribute used as the parent reference`->childrenField('children')``'children'`Attribute that holds nested children`->maxDepth(3)``-1` (unlimited)Maximum nesting depth for drag-and-drop`->maxVisibleDepth(5)``4`Maximum rendered depth in the flat list view`->searchable()``false`Show the search input and highlight matching labels`->draggable(false)``true`Enable or disable drag-and-drop reordering`->allowCrossCategory()``false`Allow nodes to move between root-level branches`->lazy()``false`Defer node loading until after first render`->asyncChildren(fn)``null`Load children on-demand when a node is expanded`->saveOrderUsing(fn)``null`Closure to persist reorder; receives the nested nodes array`->getRecordUsing(fn)``null`Custom closure to resolve a node record by its ID`->nodeActions([...])``[]`Per-node action buttons (edit, delete, custom)`->appendToolbarActions([...])`—Add buttons to the toolbar (append to defaults)---

Saving Order After Drag &amp; Drop
----------------------------------

[](#saving-order-after-drag--drop)

### Option 1 — Automatic (kalnoy/nestedset)

[](#option-1--automatic-kalnoynestedset)

If your model uses the [`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset) `NodeTrait`, the tree calls `rebuildTree()` automatically when the Save button is clicked — no extra configuration required:

```
use Kalnoy\Nestedset\NodeTrait;

class Category extends Model
{
    use NodeTrait;
}
```

```
public function tree(Tree $tree): Tree
{
    return $tree->model(Category::class)->labelField('title');
}
```

### Option 2 — Custom callback

[](#option-2--custom-callback)

```
public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Category::class)
        ->saveOrderUsing(function (array $nodes): void {
            // $nodes is the full nested array from Alpine
            foreach ($nodes as $index => $node) {
                Category::where('id', $node['id'])->update(['sort_order' => $index]);
            }
        });
}
```

If neither option is configured, a `MissingSaveOrderCallbackException` is thrown at runtime when save is triggered.

**Save button in toolbar**

The default toolbar includes a **Save** button that is hidden until a drag-drop reorder occurs. You can also add your own conditional save action:

```
use Filament\Actions\Action;

public function tree(Tree $tree): Tree
{
    return $tree
        ->appendToolbarActions([
            Action::make('save_order')
                ->label('Save')
                ->icon('heroicon-o-check')
                ->extraAttributes(['x-show' => 'hasUnsavedOrder', 'x-cloak' => true])
                ->action('saveOrder'),
        ]);
}
```

---

Node Actions
------------

[](#node-actions)

Add per-node action buttons:

```
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;

public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Category::class)
        ->nodeActions([
            EditAction::make()
                ->iconButton()
                ->icon('heroicon-o-pencil')
                ->size('sm'),
            DeleteAction::make()
                ->iconButton()
                ->icon('heroicon-o-trash')
                ->size('sm')
                ->color('danger'),
        ]);
}
```

[![Node with actions](./assets/02-node-actions.png)](./assets/02-node-actions.png)

### Custom node actions

[](#custom-node-actions)

```
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;

->nodeActions(fn (Tree $tree) => [
    Action::make('rename')
        ->iconButton()
        ->icon('heroicon-o-pencil')
        ->schema([TextInput::make('title')->required()])
        ->fillForm(fn ($record): array => is_array($record) ? $record : $record->toArray())
        ->action(function (array $data, $record, array $arguments) use ($tree): void {
            // $record is the Eloquent model or the array node
            // $arguments['nodeId'] is the node's primary key
            $record->update(['title' => $data['title']]);
        })
        ->after(fn ($livewire) => $livewire->dispatch('tree-refresh')),
])
```

### `getRecordUsing` — custom record resolution

[](#getrecordusing--custom-record-resolution)

By default, when a node action fires the plugin resolves the record from the database (for model-based trees) or the flat `records()` array (for static trees). Override this for custom lookups:

```
->getRecordUsing(function (int|string $id, Tree $tree, $livewire): mixed {
    return Category::withTrashed()->find($id);
})
```

---

Toolbar Actions
---------------

[](#toolbar-actions)

Pass `Action` or `ActionGroup` instances via `->appendToolbarActions()`:

```
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;

public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Category::class)
        ->appendToolbarActions([
            CreateAction::make('create_node')
                ->model(Category::class)
                ->schema(fn (Schema $schema) => $this->form($schema))
                ->after(fn ($livewire) => $livewire->dispatch('tree-refresh'))
                ->extraAttributes(['style' => 'margin-left: auto;']),

            ActionGroup::make([
                Action::make('import')->label('Import'),
                Action::make('export')->label('Export'),
            ])->label('More'),
        ]);
}
```

[![Toolbar Actions](./assets/02-node-actions.png)](./assets/02-node-actions.png)

---

Multiple Trees on One Page
--------------------------

[](#multiple-trees-on-one-page)

Override `trees()` instead of `tree()` to render multiple independent trees:

```
use SolutionForest\FilamentNestableTree\Filament\Pages\TreePage;
use SolutionForest\FilamentNestableTree\Tree;

class MultiTreePage extends TreePage
{
    public function trees(): array
    {
        return [
            'categories' => Tree::make()->model(Category::class)->searchable()->labelField('title'),
            'tags'       => Tree::make()->model(Tag::class)->searchable()->labelField('name'),
        ];
    }
}
```

[![Multiple tree](./assets/03-multiple-tree.png)](./assets/03-multiple-tree.png)

### Cross-tree drag &amp; drop

[](#cross-tree-drag--drop)

To allow nodes to be dragged from one named tree to another, enable `->allowCrossCategory()` and handle the `tree-cross-move` event:

```
class MultiTreePage extends TreePage
{
    public function trees(): array
    {
        $electronicsId = Category::where('title', 'Electronics')->value('id');
        $clothingId    = Category::where('title', 'Clothing')->value('id');

        $createAction = fn (string $category) => CreateAction::make('create_' . $category)
            ->iconButton()
            ->icon('heroicon-o-plus')
            ->after(fn ($livewire) => $livewire->dispatch('tree-refresh'))
            ->schema([
                TextInput::make('name')->required(),
            ])
            ->model(Tag::class)
            ->action(function (array $data, ?string $model) use ($category, $electronicsId, $clothingId): void {
                $categoryId = match ($category) {
                    'technology' => $electronicsId,
                    'science'    => $clothingId,
                    default       => null,
                };

                $model ??= Tag::class;
                $model::create([
                    'name' => $data['name'],
                    'category_id' => $categoryId,
                ]);
            });

        return [
            'technology' => Tree::make()
                ->records(fn () => Tag::where('category_id', $electronicsId)
                    ->defaultOrder()->get()->toTree()->toArray())
                ->labelField('name')
                ->allowCrossCategory()
                ->saveOrderUsing(fn (array $nodes) => Tag::rebuildTree($nodes))
                ->appendToolbarActions([$createAction('technology')]),

            'science' => Tree::make()
                ->records(fn () => Tag::where('category_id', $clothingId)
                    ->defaultOrder()->get()->toTree()->toArray())
                ->labelField('name')
                ->allowCrossCategory()
                ->saveOrderUsing(fn (array $nodes) => Tag::rebuildTree($nodes))
                ->appendToolbarActions([$createAction('science')]),
        ];
    }

    /**
     * Called automatically when a node is dragged from one tree to another.
     */
    public function handleCrossTreeMove(
        string $fromTreeKey,
        string $toTreeKey,
        int|string $nodeId,
        mixed $destinationParentId = null,
    ): void {
        $tag = Tag::find($nodeId);

        if (! $tag) {
            return;
        }

        $newCategoryTitle = $this->treeCategories[$toTreeKey] ?? null;
        $newCategoryId    = $newCategoryTitle
            ? Category::where('title', $newCategoryTitle)->value('id')
            : null;

        if ($destinationParentId) {
            $parent = Tag::find($destinationParentId);
            if ($parent) {
                $tag->appendToNode($parent)->save();
            }
        } else {
            $tag->saveAsRoot();
        }

        if ($newCategoryId) {
            $tag->update(['category_id' => $newCategoryId]);
        }

        $this->dispatch('tree-refresh');
    }
}
```

[![Cross Tree Drag](./assets/04-cross-tree-drag.png)](./assets/04-cross-tree-drag.png)

### Static records partitioned by a field

[](#static-records-partitioned-by-a-field)

Use `->records()` closures to split a single flat array across multiple trees by a partition field (e.g. `category_id`). Each tree sees only its own nodes; cross-tree drags update the partition field; saving one tree leaves the other tree's nodes untouched.

```
class CategoryPartitionedTreePage extends TreePage
{
    /** Flat node store — replace with database reads in production. */
    public static array $nodes = [];

    private const TREE_CATEGORY_MAP = ['tree1' => 1, 'tree2' => 2];

    protected $listeners = ['tree-cross-move' => 'handleCrossTreeMove'];

    public function trees(): array
    {
        return [
            'tree1' => Tree::make()
                ->labelField('title')
                ->allowCrossCategory()
                ->records(fn () => $this->asTree(
                    collect(static::$nodes)->where('category_id', 1)->values()->all()
                ))
                ->saveOrderUsing($this->saveOrderForCategory(1)),

            'tree2' => Tree::make()
                ->labelField('title')
                ->allowCrossCategory()
                ->records(fn () => $this->asTree(
                    collect(static::$nodes)->where('category_id', 2)->values()->all()
                ))
                ->saveOrderUsing($this->saveOrderForCategory(2)),
        ];
    }

    /**
     * Flatten + tag each saved node with its category, then merge back with
     * nodes that belong to other categories so nothing gets lost on save.
     */
    private function saveOrderForCategory(int $categoryId): Closure
    {
        return function (array $nodes) use ($categoryId): void {
            $saved  = collect($this->asFlatten($nodes))
                ->map(fn ($n) => array_merge($n, ['category_id' => $categoryId]))
                ->all();

            $others = collect(static::$nodes)
                ->filter(fn ($n) => ($n['category_id'] ?? null) != $categoryId)
                ->values()
                ->all();

            static::$nodes = array_merge($others, $saved);
        };
    }

    /**
     * Update the partition field (category_id) and parent_id when a node is
     * dragged between trees.  Silently ignored for unknown tree keys.
     */
    public function handleCrossTreeMove(
        string $fromTreeKey,
        string $toTreeKey,
        int|string $nodeId,
        mixed $destinationParentId = null,
    ): void {
        $destCategory = self::TREE_CATEGORY_MAP[$toTreeKey] ?? null;
        if ($destCategory === null) {
            return;
        }

        static::$nodes = collect(static::$nodes)
            ->map(function ($node) use ($nodeId, $destCategory, $destinationParentId) {
                if ((string) $node['id'] === (string) $nodeId) {
                    $node['category_id'] = $destCategory;
                    $node['parent_id']   = $destinationParentId;
                }
                return $node;
            })
            ->all();

        $this->dispatch('tree-refresh');
    }

    // ── Helpers ────────────────────────────────────────────────────────────────

    /** Flat parent_id array → nested children array. */
    private function asTree(array $flat): array
    {
        $map = [];
        foreach ($flat as $item) {
            $map[$item['id']] = $item + ['children' => []];
        }
        $tree = [];
        foreach ($map as $id => &$node) {
            if ($node['parent_id'] === null || ! isset($map[$node['parent_id']])) {
                $tree[] = &$node;
            } else {
                $map[$node['parent_id']]['children'][] = &$node;
            }
        }
        return $tree;
    }

    /** Nested children array → flat array (strips children key). */
    private function asFlatten(array $tree): array
    {
        $flat = [];
        foreach ($tree as $item) {
            $children = $item['children'] ?? [];
            unset($item['children']);
            $flat[] = $item;
            if (! empty($children)) {
                $flat = array_merge($flat, $this->asFlatten($children));
            }
        }
        return $flat;
    }
}
```

> **Key points**
>
> - `->records()` accepts a `Closure` — it is re-evaluated on every Livewire hydration so each tree always reflects the latest state of `$nodes`.
> - `saveOrderForCategory()` merges the newly-ordered nodes back with nodes from *other* categories so a save on tree1 never discards tree2's data.
> - `TREE_CATEGORY_MAP` is the single source of truth that links tree keys to partition values; add entries here when adding more trees.
> - For a database-backed version replace the `static::$nodes` array with Eloquent queries — the structure of `trees()`, `handleCrossTreeMove`, and `saveOrderForCategory` stays identical.

---

Async / Lazy Loading
--------------------

[](#async--lazy-loading)

### `->lazy()` — defer initial load

[](#-lazy--defer-initial-load)

Renders the component shell immediately and loads nodes in a second Livewire request. Useful for large trees:

```
Tree::make()->model(Category::class)->lazy()
```

### `->asyncChildren()` — expand-on-demand

[](#-asyncchildren--expand-on-demand)

Load children only when a node is expanded for the first time. The closure receives the parent node's ID:

```
Tree::make()
    ->model(Category::class)
    ->asyncChildren(function (int|string $parentId): array {
        return Category::where('parent_id', $parentId)->get()->toArray();
    })
```

When async children are enabled, the root-level nodes are loaded normally on mount. Child nodes are fetched via a Livewire call when the user expands a parent for the first time, and cached client-side for subsequent toggles.

---

Plain Eloquent Model (no NodeTrait)
-----------------------------------

[](#plain-eloquent-model-no-nodetrait)

You can use the package with any plain Eloquent model that has a `parent_id` column. No `kalnoy/nestedset` NodeTrait is required.

### Minimal schema

[](#minimal-schema)

```
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->unsignedInteger('order')->default(0);
    $table->foreignId('parent_id')->nullable()->constrained('posts')->nullOnDelete();
    $table->timestamps();
});
```

### Option A — model with a `children()` relationship

[](#option-a--model-with-a-children-relationship)

Define a self-referencing `HasMany` on the model:

```
class Post extends Model
{
    public function children(): HasMany
    {
        return $this->hasMany(Post::class, 'parent_id')->orderBy('order')->with('children');
    }
}
```

Then pass the model to the tree. The package performs a recursive eager load via the relationship on initial mount:

```
public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Post::class)
        ->labelField('name')
        ->parentKeyField('parent_id')
        ->saveOrderUsing(function (array $nodes): void {
            $this->saveOrder($nodes);
        });
}

private function saveOrder(array $nodes, ?int $parentId = null, int $start = 0): void
{
    foreach ($nodes as $index => $node) {
        Post::where('id', $node['id'])->update([
            'parent_id' => $parentId,
            'order'     => $start + $index,
        ]);
        if (! empty($node['children'])) {
            $this->saveOrder($node['children'], (int) $node['id'], 0);
        }
    }
}
```

### Option B — manual tree build with `records()`

[](#option-b--manual-tree-build-with-records)

Use `->records()` when you want full control over how the nested array is built (e.g., no relationship on the model):

```
public function tree(Tree $tree): Tree
{
    return $tree
        ->labelField('name')
        ->records(fn () => $this->buildTree(Post::orderBy('order')->get()))
        ->saveOrderUsing(function (array $nodes): void {
            $this->saveOrder($nodes);
        });
}

private function buildTree(Collection $items, mixed $parentId = null): array
{
    return $items
        ->where('parent_id', $parentId)
        ->map(fn (Post $item) => array_merge($item->toArray(), [
            'children' => $this->buildTree($items, $item->id),
        ]))
        ->values()
        ->toArray();
}
```

### Async children with a plain model

[](#async-children-with-a-plain-model)

Combine `->asyncChildren()` with `->model()`. On mount, only root nodes are returned. Children are loaded by the callback when the user expands a node:

```
->model(Post::class)
->asyncChildren(function (int|string $parentId): array {
    return Post::where('parent_id', $parentId)->orderBy('order')->get()->toArray();
})
```

---

Artisan Generators
------------------

[](#artisan-generators)

```
# Standalone Filament page
php artisan make:filament-tree-page CategoryTreePage

# Resource page (replaces ListRecords)
php artisan make:filament-tree-resource-page ManageCategoryTree

# Widget
php artisan make:filament-tree-widget CategoryTreeWidget
```

---

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](.github/SECURITY.md) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [carly](https://github.com/cklei-carly)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance88

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

4

Last Release

58d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/68211796?v=4)[Solution Forest](/maintainers/solutionforest)[@solutionforest](https://github.com/solutionforest)

---

Tags

laravelnestedsetfilamentfilament-pluginfilamentphpfilament-treesolutionforestfilament-nestedsetfilament-nestable-tree

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/solution-forest-filament-nestable-tree/health.svg)

```
[![Health](https://phpackages.com/badges/solution-forest-filament-nestable-tree/health.svg)](https://phpackages.com/packages/solution-forest-filament-nestable-tree)
```

###  Alternatives

[wsmallnews/filament-nestedset

Filament nestedset tree builder powered by kalnoy/nestedset with Filament v4 and v5 support

197.8k19](/packages/wsmallnews-filament-nestedset)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)[backstage/mails

View logged mails and events in a beautiful Filament UI.

16121.5k](/packages/backstage-mails)[marcelweidum/filament-passkeys

Use passkeys in your filamentphp app

6649.5k2](/packages/marcelweidum-filament-passkeys)[biostate/filament-menu-builder

An Elegant Menu Builder for FilamentPHP

6528.1k2](/packages/biostate-filament-menu-builder)[jibaymcs/filament-tour

Bring the power of DriverJs to your Filament panels and start a tour !

12453.6k](/packages/jibaymcs-filament-tour)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
