Lineage plugin screenshot
Dark mode ready
Multilingual support
Supports v5.x

Lineage by Devletes

Community

Hierarchies as a canvas for Filament: org charts, category trees, anything with a parent, with infolist cards you can pan, zoom and collapse.

Tags: Infolist Entry Widget
Supported versions:
5.x
Devletes avatar Author: Devletes

Package health

Automated checks of this plugin's Composer package

100 / 100
Security 100
Maintenance 100
Ecosystem 100
15 checks
  • Passed: GitHub Actions pinned to SHA
  • Skipped: GitLab CI includes pinned to SHA
  • Passed: Open security advisories
  • Passed: Dependabot PR responsiveness — No open Dependabot PRs.
  • Skipped: Renovate MR responsiveness
  • Passed: Dependabot or Renovate configured
  • Passed: Dependency update cooldown configured
  • Passed: Provides a security policy
  • Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
  • Passed: Commit and release recency — Active: last commit 0 days ago; last release 0 days ago.
  • Passed: composer.lock not committed by library — composer.lock is absent from the released dist archive.
  • Passed: Dist archive is lean
  • Passed: Current Laravel version supported — Package dependencies resolve together with current Laravel 13.0.
  • Passed: Current PHP version supported — Constraint ^8.2 supports current PHP 8.5.
  • Skipped: Current Symfony version supported
Third-party plugin. This is built by the community, not the Filament team. Filament does not review, endorse, or vet the security of plugins outside the 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 .
Powered by Plumb Last scanned 1 week ago

Documentation

Latest Version on Packagist Total Downloads License GitHub Stars

Hierarchies as a canvas for Filament — org charts, category trees, anything with a parent. Each card is an infolist schema, laid out and drawn natively in Livewire and Alpine.

  • Any record with a parent. Point it at manager_id or parent_id, or resolve the parent yourself — models or plain arrays
  • Cards are infolists. One nodeSchema() of ordinary Filament entries, shared by every node
  • Pan, zoom and collapse with a minimap, expand and collapse all, and fit-to-view
  • A layout that stays compact. Contour-packed Reingold–Tilford, so a deep branch tucks in beside a wide one instead of pushing it away
  • Broken data still draws. Self-references, missing parents and cycles are demoted to roots, never dropped
  • Vertical or horizontal, opening every level or just the first few
  • No dependencies — vanilla JavaScript on the Alpine you already have, themed off Filament's palette in light and dark
An org chart on a Filament page (light) An org chart on a Filament page (dark)

#Requirements

  • PHP ^8.2
  • Filament ^5.0 on Laravel 12 or 13

#Installation

composer require devletes/filament-lineage

Then publish Filament's assets so the stylesheet and script are served on your panels:

php artisan filament:assets

The package registers its own stylesheet and script with Filament's asset manager — there is nothing to add to your custom Tailwind theme.

#Quick start

TreeEntry is an infolist entry, so it goes anywhere a schema does — a page, a resource's view page, a modal:

use Devletes\Lineage\Infolists\Components\TreeEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;

public function content(Schema $schema): Schema
{
    return $schema->components([
        TreeEntry::make('org')
            ->records(fn () => Employee::query()->get())
            ->parentKey('manager_id')
            ->nodeUrl(fn (Employee $record) => EmployeeResource::getUrl('view', ['record' => $record]))
            ->nodeSchema([
                TextEntry::make('name')->hiddenLabel()->weight(FontWeight::SemiBold),
                TextEntry::make('title')->hiddenLabel()->color('gray'),
            ]),
    ]);
}
An org chart with the first two levels open (light) An org chart with the first two levels open (dark)

#Records and parents

Hand over a flat set of records and say how each one points at its parent. A flat set, rather than a children relationship, is what keeps it to one query — eager-load what the cards need and pass the lot:

TreeEntry::make('org')
    ->records(fn () => Employee::query()->with('department')->get())
    ->parentKey('manager_id')

records() takes a collection, an array or a closure returning either. Records can be models or plain arrays; a model's card is bound to the record, an array's to its values, so TextEntry::make('name') reads the same either way.

To work the parent out yourself, use parentUsing() and return a key, a model, or null for a root:

TreeEntry::make('org')
    ->records(fn () => Employee::query()->with('lineManager')->get())
    ->parentUsing(fn (Employee $record) => $record->lineManager)

A tree can mix kinds of record — departments with the people in them, say. Their primary keys will collide, so give each node a namespaced key with recordKey():

TreeEntry::make('structure')
    ->records(fn () => $departments->concat($people))
    ->recordKey(fn ($record) => class_basename($record).':'.$record->getKey())
    ->parentUsing(fn ($record) => $record instanceof Employee
        ? 'Department:'.$record->department_id
        : null)

#Broken hierarchies

Real reporting data arrives broken, and none of it should take a page down. Three cases are repaired before anything is drawn, each demoted to a root so the record still appears:

  • a node pointing at itself
  • a node pointing at an id that is not in the set — filtered out, soft-deleted, outside the tenant
  • a genuine cycle, A reports to B reports to A

#Cards

nodeSchema() is one schema shared by every card. nodeUrl() makes the whole card a link:

TreeEntry::make('org')
    ->nodeSchema([
        TextEntry::make('name')->hiddenLabel()->weight(FontWeight::SemiBold)->lineClamp(1),
        TextEntry::make('title')->hiddenLabel()->color('gray')->lineClamp(1),
    ])
    ->nodeUrl(fn (Employee $record) => EmployeeResource::getUrl('view', ['record' => $record]))

#Node size is fixed, deliberately

Every card in a tree is the same size, declared up front:

->nodeWidth(240)->nodeHeight(96)

That single constraint is what keeps the package small. Layout becomes arithmetic over known constants — no render → measure → reposition cycle, no layout shift on first paint, and collapsing a branch is a pure recompute with no round trip.

The trade is that content has to fit its box, so keep node schemas short and lean on lineClamp(). Node schemas render with their gap collapsed for the same reason; a schema's default spacing would overflow a fixed-height card on its own.

#Avatars

An avatar straddling the card's top edge is the classic org chart look, and the edge from the parent ends neatly behind it. A ViewEntry renders its partial with no wrapper around it, so nothing clips the overhang:

ViewEntry::make('avatar')->view('org-chart.avatar'),
{{-- resources/views/org-chart/avatar.blade.php --}}
<div class="fi-lineage-avatar">
    @if ($getRecord()->photo_url)
        <x-filament::avatar :src="$getRecord()->photo_url" size="lg" />
    @else
        <div class="fi-lineage-avatar-fallback">{{ $getRecord()->initials }}</div>
    @endif
</div>

fi-lineage-avatar rises by its overhang plus the card's padding and rings whatever it wraps in the card's own colour. fi-lineage-avatar-fallback is a matching disc for initials, coloured from the panel's primary. Give the row enough vertical nodeGap() to hold the overhang.

If the overhanging content has to be a TextEntry instead, add the fi-lineage-bleed class. Filament's text entry clips its overflow for lineClamp(), and the class frees it for that one entry:

TextEntry::make('badge')->extraAttributes(['class' => 'fi-lineage-bleed'])

#Layout

#Opening only the first few levels

A whole organisation drawn at once fits to a few percent and reads as dots. initialDepth() opens that many levels and leaves the rest a click away — 2 shows the roots and their direct children:

TreeEntry::make('org')->initialDepth(2)

Each parent carries a toggle, and the cards inside a collapsed branch are never mounted, so a large tree stays fast to pan. The markup still ships with the page, so opening a branch needs no round trip.

One branch opened under the CTO (light) One branch opened under the CTO (dark)

collapsible(false) drops the toggles and the expand / collapse all buttons, and draws every level.

#Horizontal trees

horizontal() grows the tree rightward from its roots, which suits deep, narrow hierarchies like a product catalogue:

TreeEntry::make('categories')
    ->records(fn () => Category::query()->get())
    ->parentKey('parent_id')
    ->horizontal()
    ->nodeWidth(200)
    ->nodeHeight(64)
    ->nodeGap(horizontal: 64, vertical: 16)
A product category tree drawn horizontally (light) A product category tree drawn horizontally (dark)

#Spacing

nodeGap() sets the space between cards along each screen axis — horizontal left to right, vertical top to bottom — whichever way the tree grows. Either argument can be left out to keep its default:

->nodeGap(vertical: 96)

On a vertical tree, a generous vertical gap is what lets the drop from parent to child carry the relationship; on a horizontal tree, the horizontal gap does that job.

#Canvas height

The canvas is 32rem tall unless told otherwise. Any CSS length works:

->canvasHeight('calc(100vh - 20rem)')

Drag the canvas to pan, and scroll or pinch to zoom around the cursor. The toolbar expands and collapses every branch, zooms in and out, and fits the whole tree back into view. The canvas frames itself on load and whenever it is resized, until the reader pans or zooms — after that the view is theirs.

The minimap in the corner shows where the visible area sits in the whole tree:

->minimap(width: 200, height: 140)
->minimap(false)
The whole organisation fitted to the canvas (light) The whole organisation fitted to the canvas (dark)

#Configuration

config/lineage.php sets the defaults every tree starts from:

php artisan vendor:publish --tag=lineage-config
return [
    'height' => '32rem',     // default canvas height
    'zoom' => [
        'min' => 0.25,       // how far wheel and button zoom can go out
        'max' => 2.0,        // and in; fit-to-view may go below min for a large tree
    ],
];

#Theming

Every colour resolves through Filament's palette variables, so the canvas follows your panel's primary colour and its light and dark themes without being told. To change it further, override the --fi-lineage-* variables in your theme stylesheet:

html .fi-lineage {
    --fi-lineage-accent: var(--success-600);
    --fi-lineage-edge: var(--gray-300);
}

html .fi-lineage-avatar {
    --fi-lineage-avatar-overhang: 1rem;
}

The canvas variables are surface, border, grid, node-bg, text, edge, muted, accent and scrim (the wash over the minimap). Avatars read avatar-overhang, avatar-bg and avatar-color.

Or publish the views to change the markup:

php artisan vendor:publish --tag=lineage-views

#Translations

The toolbar's labels go through lineage::lineage.*. Publish the language files to reword them or add a locale:

php artisan vendor:publish --tag=lineage-translations

Then edit or add files under lang/vendor/lineage/.

#API reference

Method Default What it does
records($records) — A collection, array or closure of the records to draw
parentKey($attribute) — The attribute holding each record's parent key
parentUsing($callback) — Resolve the parent yourself: return a key, a model, or null for a root
recordKey($attribute) primary key The attribute, or a closure, giving each record's key
nodeSchema($components) — One schema, shared by every card
nodeUrl($callback) — Makes the whole card a link
nodeWidth($px) / nodeHeight($px) 240 / 96 Card size, for the whole tree
nodeGap($horizontal, $vertical) 24 / 56 Space between cards on each screen axis
orientation($orientation) / horizontal() vertical Which way the tree grows
collapsible($condition) true Per-branch toggles and the expand / collapse all buttons
initialDepth($levels) null Levels open on first paint; null opens everything
minimap($condition, $width, $height) true, 160, 110 The overview in the corner
canvasHeight($height) 32rem Any CSS length

Every option accepts a closure.

#Testing

composer install
composer test

The suite runs on Testbench with the real Filament stack, against PHP 8.2–8.4 on Laravel 12 and 13.

#Need something custom?

We build production Filament panels and plugins for teams that want to ship fast without compromising on polish. If you need a custom feature, an extended variant of this package, or a fully bespoke component built for your stack, we can help.

Typical engagements: new Filament plugins, custom resources/widgets/actions, theme + UX work, integrations with your existing services, and one-off tailored forks of our open-source packages.

#Have you seen our Orbit theme?

Orbit is our dark-first theme for Filament 5. Every panel surface is restyled — sidebar, topbar, forms, tables, modals, notifications.

Orbit theme, light mode Orbit theme, dark mode

Check it out here →

#Credits

#License

MIT. See LICENSE.md.