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.
- 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
composer require jibaymcs/tabbedAdd the plugin's views to your custom theme CSS file:
@source '../../../../vendor/jibaymcs/tabbed/resources/**/*.blade.php';Register the plugin in your PanelProvider:
use JibayMcs\Tabbed\TabbedPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
TabbedPlugin::make(),
]);
}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
}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
]);
}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
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 saveControl 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 recordsPer-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.
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')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.
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 |
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)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").
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.
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.
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 settingThe 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.
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.
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.
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.
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.
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.
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">.
Please see CHANGELOG for more information on what has changed recently.
The MIT License (MIT). Please see License File for more information.