octopyid/filament-tabify
Filament Tabify splits long Filament form schemas into clean, navigable tabs, so admins scroll less and find fields faster. Group fields, set labels and icons, and add better structure to any Filament resource or page with minimal configuration.
Precondition check before you type composer require. This package is a presentation-only layer over Filament's tabs; if the target project isn't already a Filament panel, stop here. You need a pinned Filament version in composer.json, PHP/Laravel versions inside the package's support window, and a non-critical internal dashboard to spike on.
Step 1 — resolve the scope ambiguity first. The repository description says "Tabbed Widgets," while the package's own description text pitches "tabbed layouts for Filament forms." These are different integration points (a dashboard widget container vs. a form-schema component) with different blast radii. Read these four things in order before writing any code:
composer.json — the declared filament/filament constraint. Compare it against your locked version; a ^3.0 constraint on a Filament 4 project is your answer.src/ — the service provider and any Plugin class. Note whether it registers a panel plugin, publishes config, or publishes assets.Filament\Widgets\Widget, implement a form component, or is it a Blade view component?resources/views/ — the actual markup. Confirm whether it wraps <x-filament::tabs> (good) or ships its own Alpine tab implementation (more surface to own).If it turns out to target forms, you almost certainly don't need it: Filament ships Filament\Forms\Components\Tabs (v3) / Filament\Schemas\Components\Tabs (v4) natively, with validation, ->persistTabInQueryString(), and accessibility handled for you. That native component is the answer, and the dependency is dead weight.
Step 2 — install and pin, don't float.
composer require octopyid/filament-tabify
Then immediately edit composer.json to an exact version ("octopyid/filament-tabify": "1.0.0"), not a caret range. Zero dependents and a single maintainer means no one else will report the breakage to you.
Step 3 — register on one panel only. If it ships a plugin:
// app/Providers/Filament/AdminPanelProvider.php
->plugins([
\OctopyID\FilamentTabify\TabifyPlugin::make(),
])
Confine it to the internal/staff panel. Do not let it leak into a customer-facing panel by registering it in a shared provider.
Step 4 — first use case. Pick a dashboard with six or more widgets that users actually scroll past, and where widgets cluster into named domains — Revenue / Support / Ops. Arbitrary tab grouping ("Tab 1, Tab 2") is worse than a scroll. Convert three to five widgets into two tabs, nothing more, and keep the change reversible in a single PanelProvider line.
Step 5 — prove two things before you go further. (a) Hidden tabs do not amplify query volume or Livewire request count versus your untabbed baseline — check with Telescope or Debugbar on a cold load. (b) The tabs survive a Filament theme build without a custom Vite pipeline change. Those two answers decide whether this is a one-day spike or a two-week detour.
Container widget, children stay dumb. The only pattern worth standardizing: one widget class whose Blade view renders the tab bar and conditionally renders child widgets. Children must not know they're in a tab — no tab-specific base class, no tab-aware traits. That keeps them portable to the plain dashboard grid when you replace this package with first-party code.
{{-- resources/views/widgets/tabbed-dashboard.blade.php --}}
<div>
<x-filament::tabs>
<x-filament::tabs.item
wire:click="$set('activeTab', 'revenue')"
:active="$activeTab === 'revenue'"
>
Revenue
</x-filament::tabs.item>
{{-- ... --}}
</x-filament::tabs>
<div class="mt-6">
@if ($activeTab === 'revenue')
@livewire(\App\Filament\Widgets\RevenueChart::class)
@livewire(\App\Filament\Widgets\MrrStats::class)
@endif
</div>
</div>
Mount lazily, unconditionally. The default @if above already prevents mounting, but if you instead render all tabs and hide with CSS (which is what many tab implementations, possibly this one, do), every child widget instantiates and polls on page load. Verify which behaviour you got by counting Livewire requests on load with all tabs closed. If the package hides rather than suspends, add #[Lazy] to child widgets or wrap in @livewire(..., lazy: true).
Deep-linking via the query string. Tab state is client-side unless you wire it. Use Livewire's #[Url] so a support engineer can send a colleague a link straight to the Support tab:
use Livewire\Attributes\Url;
#[Url(as: 'tab', history: true)]
public string $activeTab = 'revenue';
This also gives you back-button behaviour for free, and is the one thing the package is least likely to do for you.
Polling discipline. Any child widget with protected static ?string $pollingInterval = '5s' keeps firing while hidden if it's mounted. Either set the interval to null on widgets that live in tabs, or gate it:
public function getPollingInterval(): ?string
{
return $this->activeTab === 'revenue' ? '10s' : null;
}
On a busy dashboard this single line is the difference between three polling widgets and nine.
Grid and column spans. Widgets inside a tab lose the dashboard's responsive grid context. Set protected int|string|array $columnSpan = 'full' on child widgets (or replicate the grid with a grid grid-cols-12 gap-6 wrapper and per-widget spans) so charts don't collapse to a sliver inside the tab pane.
Permissions per tab. Tabs are a natural place to attach authorisation, and it's cheap:
->visible(fn (): bool => auth()->user()->can('viewRevenue'))
Filter both the tab item and the child widget (canView()), otherwise a user can hit a tab whose widgets all return 403 and see an empty pane.
Vendoring pattern, if the spike passes. Don't depend on upstream directly. Fork under packages/octopyid/filament-tabify in your monorepo or app/Support/Tabify, add a path repository, pin it, and add one smoke test:
it('renders the tabbed dashboard', function () {
$this->actingAs(User::factory()->create())
->get(Dashboard::getUrl(panel: 'admin'))
->assertOk()
->assertSee('Revenue');
});
That test is your entire early-warning system for the next Filament bump — silent visual degradation is the normal failure mode here, and nothing else will catch it.
Have the exit path costed now. A blade wrapper around <x-filament::tabs> plus a $activeTab property and three @if blocks is roughly a hundred lines. If the package fights you on lazy mounting, deep-linking, or column spans during the spike, write the wrapper instead of working around it.
The docs disagree about what the package does. "Tabbed Widgets" in the headline, "tabbed forms" in the description body. Do not estimate work or put this on a roadmap line until someone has opened src/ and confirmed. This is a scoping risk, not a documentation nit — a form-layout tool and a dashboard container have different owners, different QA, and different rollback stories.
Hidden tabs probably still mount. This is the single highest-value thing to test. Tab containers that hide with CSS keep every child widget alive: their mount() runs, their booted() runs, their polling runs, their aggregate queries run. On a dashboard with thirty widgets across five tabs you've made the page look faster while making it measurably heavier. Measure mount count and query count against an untabbed baseline before claiming an improvement.
Chart widgets break inside hidden containers. Chart.js sizes to the container at render time. A chart inside display:none gets zero width and renders blank, then fails to recover when the tab is shown. Fix by dispatching a resize event on tab activation, or by only mounting the chart widget when its tab is active (@if rather than hidden).
Table widgets have the same problem, plus pagination. A Filament table in a hidden container computes wrong widths and can paginate against stale state. Suspend rather than hide.
Tab state is not persisted across refresh unless you built it. Filament's native tabs use ->persistTabInQueryString(); a widget container won't do this by itself. If a user reports "it jumps back to the first tab," this is why.
Version metadata is not trustworthy. The stated last release (2026-09-13) is in the future. Verify the tagged commit, the release cadence over the last six months, and whether CI actually exists before treating the package as maintained. One star, zero dependents, single maintainer — you will be the largest consumer on day one.
Pin both sides of the matrix. octopyid/filament-tabify and filament/filament both exact versions. Treat tabify as a named breakage candidate during every Filament major and minor bump, and run the upgrade against a branch first. Failure mode is visual and silent: wrong columnSpan math, orphaned assets, tabs that render but don't switch.
Assets and theming. Determine during the spike whether it publishes assets or expects a custom Vite theme. If it needs a theme build, your front-end pipeline is in scope and the "one-day spike" estimate is wrong.
Accessibility is on you. Verify role="tab" / role="tablist", aria-selected, and arrow-key navigation. Filament's own <x-filament::tabs> handles this; a bespoke Alpine implementation in a low-star package may not. If keyboard navigation is a stated requirement, budget to fix it rather than assume it works.
Where to debug, in order. (1) php artisan about and the resolved filament/filament constraint — most "it doesn't render" reports are version mismatch. (2) The published views under resources/views/vendor/; confirm whether the package's views even made it out. (3) Livewire::test() on the container widget to inspect activeTab state independent of the browser. (4) Telescope's Livewire request panel for the mount amplification question.
Scope discipline. Tabs change no data, no metrics, and no query behaviour. Never let this package appear as justification for an analytics or reporting roadmap item. It buys legibility, not capability — and at one star, it buys it with a maintenance obligation you should assume you own within two quarters.
How can I help you explore Laravel packages today?