jibaymcs / tabbed
A FilamentPHP plugin to manage records in manageable tabs
Package info
Language:JavaScript
pkg:composer/jibaymcs/tabbed
Fund package maintenance!
Requires
- php: ^8.2
- filament/filament: ^5.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
README
A FilamentPHP v5 plugin that brings IDE/browser-style tabs to your panel. Open resource pages (Edit, View, Create, List) in tabs, switch between them instantly without losing state, and organize your workflow with drag & drop, renaming, and context menus.
Features
- Open any Filament resource page in a tab
- Instant tab switching (no page reload, state preserved)
- Drag & drop tab reordering
- Inline tab renaming (double-click)
- Right-click context menu (rename, close, close others, close all)
- Middle-click to close tabs (opt-in)
- Configurable tab bar position (topbar, page start, content start, etc.)
- LocalStorage persistence across page navigations
- Background tab opening
- Custom tab labels
- Custom tab colors (accent, background, text) with Filament Color support
- Hover cards on tabs (rich tooltip with custom content on hover)
- Lazy loading & destroy inactive (performance optimization)
- Dropdown mode (compact button replacing the full tab bar)
- Dirty state detection with unsaved changes confirmation modal
- Post-save redirect interception (stay in tab after save, create-to-edit transformation)
- Pinned tabs (anchored left, protected from bulk close, visually distinct)
- Tab search in overflow/dropdown menu (filter by name, keyboard navigation)
- Tab duplication via context menu
- Granular permissions (global + per-tab with
$recordclosures) - Keyboard shortcuts (configurable, no browser conflicts)
- Reopen last closed tab (history stack, context menu + shortcut)
- Dark mode support
- Translations: English, French & Spanish
Installation
composer require jibaymcs/tabbed
Add the plugin's views to your custom theme CSS file:
@source '../../../../vendor/jibaymcs/tabbed/resources/**/*.blade.php';
Setup
Register the plugin in your PanelProvider:
use JibayMcs\Tabbed\TabbedPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ TabbedPlugin::make(), ]); }
Usage
Option 1: Automatic with trait
Add HasTabbedActions to your Resource to automatically include the "Open in tab" action on every table row:
use JibayMcs\Tabbed\Traits\HasTabbedActions; class UserResource extends Resource { use HasTabbedActions; // Your resource code — no other changes needed }
Option 2: Manual action
Add OpenInTabAction manually in your table configuration for more control:
use JibayMcs\Tabbed\Actions\OpenInTabAction; public static function table(Table $table): Table { return $table ->recordActions([ OpenInTabAction::make(), // ...other actions ]); }
Option 3: Row click
Make clicking a table row open the record in a tab instead of navigating to the edit page:
public static function table(Table $table): Table { return $table ->recordUrl(null) ->recordAction('tabbed') ->recordActions([ OpenInTabAction::make() ->hiddenLabel() ->background() ->tabName(fn ($record) => "Ticket #{$record->id}"), ]); }
recordUrl(null)— disables the default link on the rowrecordAction('tabbed')— clicking a row triggers theOpenInTabActionvia Livewirebackground()— opens the tab without switching to ittabName()— custom label for the tab
Action options
OpenInTabAction::make() ->tabbedPage('view') // Target page: edit, view, create, index (default: from config) ->background() // Open tab without switching to it ->tabName(fn ($record) => $record->name) // Custom tab label ->resource(UserResource::class) // Explicit resource (auto-detected by default) ->tabColor(Color::Red) // Accent color (left border indicator) ->tabBackground(Color::Red) // Background color ->tabTextColor(Color::Red) // Text color ->confirmOnClose() // Ask confirmation before closing if dirty ->closeOnSave() // Auto-close the tab after a successful save
Per-tab permissions
Control what users can do with individual tabs. Accepts bool or a Closure receiving $record for conditional logic:
OpenInTabAction::make() ->canReorder(false) // Prevent drag & drop for this tab ->canRename(fn ($record) => $record->is_editable) // Conditional rename ->canPin(fn ($record) => $record->is_important) // Conditional pin ->canDuplicate(true) // Allow duplication (default) ->canClose(fn ($record) => ! $record->is_locked) // Prevent closing locked records
Per-tab permissions combine with global settings (allowReorder, allowRename, etc.) on TabbedPlugin. The global setting is the master switch: if it's off, the per-tab setting is ignored. If the global is on, the per-tab closure decides.
Tab colors
Customize tab appearance per action. Accepts Filament Color palettes, hex values, or any CSS color string:
use Filament\Support\Colors\Color; // Filament Color palette (shade picked automatically) OpenInTabAction::make() ->tabColor(Color::Red) // border: shade 500 ->tabBackground(Color::Red) // background: shade 50 ->tabTextColor(Color::Red) // text: shade 700 // Specific shade from a palette OpenInTabAction::make() ->tabColor(Color::Blue[600]) // Hex, rgb, rgba OpenInTabAction::make() ->tabColor('#ef4444') ->tabBackground('rgba(254, 242, 242, 0.8)') ->tabTextColor('#991b1b')
Hover cards
Display a rich tooltip when hovering over a tab. The content is fully customizable and has access to the $record:
use JibayMcs\Tabbed\Enums\HoverCardPosition; use Illuminate\Support\HtmlString; // Blade view with record data OpenInTabAction::make() ->hoverCardContent(fn ($record) => view('partials.tab-preview', ['record' => $record])) ->hoverCardPosition(HoverCardPosition::Bottom) // Inline HTML OpenInTabAction::make() ->hoverCardContent(fn ($record) => new HtmlString("<strong>{$record->name}</strong><br>{$record->email}")) ->hoverCardDelay(400) // Delay before showing (default: 600ms) ->hoverCardLeaveDelay(300) // Delay before hiding (default: 500ms) // Plain text OpenInTabAction::make() ->hoverCardContent(fn ($record) => "#{$record->id} - {$record->name}") ->hoverCardPosition(HoverCardPosition::Top) // Disable hover card OpenInTabAction::make() ->hoverCard(false)
Available positions: Top, TopStart, TopEnd, Bottom, BottomStart, BottomEnd, Left, Right.
Security note: Hover card content is rendered as raw HTML (
x-html). If you include user-provided data, make sure to escape it withe()orhtmlspecialchars()to prevent XSS vulnerabilities.
The hover card stays visible when moving the cursor from the tab to the card. It also works on overflow dropdown items.
JavaScript events
You can open/close tabs programmatically from anywhere:
// Open a tab window.dispatchEvent(new CustomEvent('tabbed:open', { detail: { resource: 'App\\Filament\\Resources\\UserResource', page: 'edit', recordId: 5, label: 'Custom label', // optional background: false, // optional } })); // Close a tab window.dispatchEvent(new CustomEvent('tabbed:close', { detail: { id: 'tab-uuid' } }));
Events dispatched by the plugin:
| Event | Payload | Description |
|---|---|---|
tabbed:tab-opened |
{ tab } |
A tab was opened |
tabbed:tab-closed |
{ tab } |
A tab was closed |
tabbed:tab-activated |
{ tabId } |
A tab was activated |
tabbed:tab-deactivated |
{ tabId } |
Active tab was toggled off |
tabbed:all-closed |
— | All tabs were closed |
Configuration
Plugin options
Configure via fluent methods in your PanelProvider:
TabbedPlugin::make() ->defaultPage('view') // Default page on open (default: edit) ->renderHook(PanelsRenderHook::TOPBAR_LOGO_AFTER) // Tab bar position (default: TOPBAR_LOGO_AFTER) ->persistKey('my_panel_tabs') // localStorage key (default: tabbed_tabs) ->middleClickToClose() // Close tabs with middle mouse button (default: off) ->showTabIcons(false) // Hide resource icons in tabs (default: true) ->lazyLoad() // Only load tab content on first activation (default: off) ->destroyInactive() // Destroy inactive tab components to save memory (default: off) ->confirmClose() // Confirm before closing tabs with unsaved changes (default: off) ->interceptRedirects() // Block post-save redirects inside tabs (default: on) ->allowReorder(false) // Disable drag & drop reordering (default: on) ->allowRename(false) // Disable inline tab renaming (default: on) ->allowPin(false) // Disable tab pinning (default: on) ->allowDuplicate(false) // Disable tab duplication (default: on) ->allowCloseOthers(false) // Hide "Close others" from context menu (default: on) ->allowCloseAll(false) // Hide "Close all" from context menu (default: on) ->keyboardShortcuts() // Enable keyboard shortcuts with defaults (default: off)
Keyboard shortcuts
Enable keyboard shortcuts for power-user navigation. Disabled by default to avoid unexpected behavior.
// Enable with default shortcuts TabbedPlugin::make()->keyboardShortcuts() // Custom shortcuts TabbedPlugin::make()->keyboardShortcuts( nextTab: 'ctrl+alt+right', // Next tab (default) prevTab: 'ctrl+alt+left', // Previous tab (default) closeTab: 'alt+w', // Close active tab (default) reopenTab: 'alt+shift+t', // Reopen last closed tab (default) ) // Disable TabbedPlugin::make()->keyboardShortcuts(false)
Default shortcuts:
| Action | Shortcut |
|---|---|
| Next tab | Ctrl+Alt+Right |
| Previous tab | Ctrl+Alt+Left |
| Close active tab | Alt+W |
| Reopen last closed | Alt+Shift+T |
Shortcuts are ignored when an input, textarea, or select is focused. They use Alt as the primary modifier to avoid conflicts with browser shortcuts (Ctrl+Tab, Ctrl+W, etc.).
Closed tabs are stored in a session-only history stack (max 10). You can also reopen them via the right-click context menu ("Reopen closed tab").
Performance: Lazy loading & destroy inactive
By default, all open tabs have their Livewire components created immediately. For better performance with many tabs:
// Lazy load: components are created only when a tab is activated for the first time. // Once loaded, they stay in memory (state preserved on switch). TabbedPlugin::make()->lazyLoad() // Destroy inactive: only the active tab has a Livewire component in the DOM. // Switching tabs destroys the previous component and creates the new one. // Saves memory but loses form state on switch. Implies lazyLoad. TabbedPlugin::make()->destroyInactive() // Keep alive: keep the N most recently visited tabs in memory (LRU). // Tabs beyond this limit are destroyed. Combines fast switching with memory savings. TabbedPlugin::make()->destroyInactive(keepAlive: 3)
A loading spinner appears in the tab panel while the Livewire component loads, and a small loading indicator is shown on the tab itself.
Dropdown mode
Replace the full tab bar with a compact dropdown button:
TabbedPlugin::make() ->hasDropdown() // Enables dropdown mode (default icon + badge) ->hasDropdown( // Full customization icon: 'phosphor-tabs-duotone', // Custom icon (default: heroicon-m-squares-2x2) label: 'Tabs', // Optional text label countBadge: true, // Show tab count badge (default: true) color: 'primary', // Filament color name (default: primary) outlined: false, // Outlined style (default: false) ) // Icon only, no badge, outlined TabbedPlugin::make()->hasDropdown(countBadge: false, outlined: true) // Label only, no icon TabbedPlugin::make()->hasDropdown(icon: null, label: 'My tabs') // Icon + label TabbedPlugin::make()->hasDropdown(icon: 'heroicon-m-squares-2x2', label: 'Tabs')
Clicking the button opens a dropdown listing all tabs with icons, active indicator, close buttons, and hover cards. All existing features (lazy load, middle-click, persistence) work in dropdown mode.
Dirty state & close confirmation
The plugin detects unsaved changes in tab forms. When a form field is modified, an orange dot appears on the tab. If confirmation is enabled, closing a dirty tab shows a Filament-style modal instead of closing immediately.
// Global: all dirty tabs ask confirmation before closing TabbedPlugin::make()->confirmClose() // Per-tab: only specific actions ask confirmation OpenInTabAction::make()->confirmOnClose() // Both can be combined: global acts as a default, per-tab overrides TabbedPlugin::make()->confirmClose() // A tab without ->confirmOnClose() will still ask because of the global setting
The dirty state resets automatically after a successful save (save or create Livewire calls). The confirmation modal also appears for "Close others" and "Close all" context menu actions when dirty tabs are involved.
Redirect interception
By default, saving a form inside a tab stays in the tab instead of following Filament's redirect (which would navigate away from the tab system). This works for both Edit and Create pages:
- Edit page: after save, the redirect is blocked and the user stays in the tab
- Create page: after creating a record, the tab automatically transforms into an Edit tab for the new record (new tab ID, updated label and record ID)
// Disable redirect interception globally (saves redirect normally) TabbedPlugin::make()->interceptRedirects(false) // Auto-close a tab after successful save (serial processing workflow) OpenInTabAction::make()->closeOnSave()
Notifications and other Livewire effects are preserved — only the redirect is blocked.
Pinned tabs
Right-click a tab and select "Pin" to pin it. Pinned tabs are visually distinct (primary background + pin icon) and anchored to the left of the tab bar.
Pinned tab protections:
- "Close others" keeps pinned tabs + the target tab
- "Close all" only closes unpinned tabs
- Pinned tabs are never evicted by
destroyInactiveLRU - Drag & drop is constrained: pinned tabs can only be reordered among themselves
The close button (x) still works on pinned tabs — pinning protects against bulk close, not individual close. Pin state is persisted in localStorage.
Tab search
When the overflow dropdown (or dropdown mode menu) contains 5 or more tabs, a search field appears at the top. Type to filter tabs by name in real time (case-insensitive, partial match). Use arrow keys to navigate results and Enter to activate the highlighted tab. Escape clears the search, or closes the menu if the search is already empty.
Tab duplication
Right-click a tab and select "Duplicate" to open the same resource/page/record in a new tab. The duplicate opens right after the original with a numbered suffix (e.g. "User #5 (2)"). Colors, hover card, and other settings are copied from the original.
Config file
Publish the config file for project-wide defaults:
php artisan vendor:publish --tag="tabbed-config"
// config/tabbed.php return [ 'default_page' => 'edit', 'persist_key' => 'tabbed_tabs', ];
Plugin fluent methods take priority over config file values.
Tab bar position
The tab bar can be placed at any Filament render hook position:
use Filament\View\PanelsRenderHook; // In the topbar (after the logo) TabbedPlugin::make()->renderHook(PanelsRenderHook::TOPBAR_LOGO_AFTER) // At the start of the page content TabbedPlugin::make()->renderHook(PanelsRenderHook::PAGE_START) // Inside the main content area TabbedPlugin::make()->renderHook(PanelsRenderHook::CONTENT_START)
The tab bar is rendered at the chosen position, while the tab content panels always render inside <main class="fi-main">.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.