Features

Sticky (Pinned) Columns

Pin columns so they stay visible during horizontal scroll — dev defaults, user pinning, and a customizable toolbar trigger.

Sticky columns stay frozen on the left while the rest of the table scrolls horizontally.

Dev-declared default

Mark a column sticky with ->sticky():

TextColumn::make('name')->sticky();

This column is pinned for everyone out of the box.

User-controlled pinning

Enable ->stickableColumns() on the table. A Pin columns dropdown appears in the toolbar (before the column-manager trigger).

  • Checkboxes draft which columns are pinned.
  • Select all / Deselect all shortcuts are included.
  • Changes apply only when the user clicks Apply (the button is disabled when nothing changed).
  • Closing the panel without Apply keeps the draft in memory for that page session.
public function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('name')->sticky(), // seeds initial selection
            TextColumn::make('email'),
            TextColumn::make('created_at'),
        ])
        ->dragReorderableColumns()
        ->stickableColumns();
}

Any ->sticky() calls seed the initial selection. Once a user applies their own choice, that selection is remembered.

The panel renders next to Filament's column-manager trigger, which Filament only draws when the table has a column manager or a filters trigger. On a table with neither, the panel falls back to the position beside the search field.

Unpin from the header

A pinned column that the user pinned themselves carries a clickable pin icon in its header. Clicking it unpins that column straight away — no panel, no Apply — and the new selection persists like any other change.

Columns declared with ->sticky() show the same pin, but it is not clickable: those are the developer's defaults and toggleColumnSticky() refuses to remove them, so the header offers no control that would do nothing.

ColumnPin iconClick to unpin
->sticky()YesNo — pinned for everyone
Pinned by the userYesYes
Not pinnedNo

Customize the trigger

Customize the toolbar button with ->stickyManagerTriggerAction(), mirroring Filament's columnManagerTriggerAction():

use Filament\Actions\Action;

public function table(Table $table): Table
{
    return $table
        ->columns([...])
        ->stickableColumns()
        ->stickyManagerTriggerAction(fn (Action $action) => $action
            ->label('Pinned columns')
            ->icon('heroicon-o-map-pin')
            ->tooltip('Choose which columns stay visible'));
}

The default trigger is a small gray Filament button labeled Pin columns (translatable via resources/lang).

Behaviour notes

  • Pinned columns are left-pin only in the current version.
  • Pinned headers show a small pin indicator, clickable to unpin on user-pinned columns; resize handles work on sticky columns (offsets refresh after drag).
  • Sticky columns are excluded from drag-to-reorder; columns cannot be dropped before a sticky column.
  • Header and body cells use opaque backgrounds so horizontally scrolled content does not show through.

Style sticky backgrounds

Pinned cells need opaque backgrounds. By default they resolve to Filament's own table surface variables, in both light and dark mode — nothing to configure:

VariableUsed forDefault (light)Default (dark)
--resized-sticky-header-bgPinned header cellsvar(--gray-50)color-mix(in oklab, var(--color-white) 5%, var(--gray-900))
--resized-sticky-cell-bgPinned body cells (default row)var(--color-white)var(--gray-900)
--resized-sticky-cell-bg-emphasisPinned body cells (striped, hover, selected)var(--gray-50)color-mix(in oklab, var(--color-white) 5%, var(--gray-900))

A custom Filament theme that changes --gray-* or --color-white is picked up automatically.

Striped tables

On a table using Filament's ->striped(), the tinted rows are painted by --resized-sticky-cell-bg-emphasis — the same variable that covers hovered and selected rows. --resized-sticky-cell-bg only applies to plain rows.

public function table(Table $table): Table
{
    return $table
        ->striped()
        ->stickableColumns();
}
:root {
    /* plain rows */
    --resized-sticky-cell-bg: var(--color-white);
    /* striped, hovered, and selected rows */
    --resized-sticky-cell-bg-emphasis: var(--gray-50);
}

Filament's own stripe is a translucent tint layered over the row (color-mix(..., transparent) in dark mode). A pinned cell cannot be translucent — the columns scrolling underneath would show through — so give this variable an opaque colour that matches the stripe. The defaults already do this by compositing the tint over the table surface.

To use your own colours, declare the variables in your panel's theme — resources/css/filament/{panel}/theme.css. Light mode goes on :root, dark mode on .dark (Filament toggles that class on <html>):

/* resources/css/filament/admin/theme.css */
@import '../../../../vendor/filament/filament/resources/css/theme.css';

@source '../../../../app/Filament/**/*';

:root {
    --resized-sticky-header-bg: var(--gray-100);
    --resized-sticky-cell-bg: var(--gray-50);
    --resized-sticky-cell-bg-emphasis: var(--gray-100);
}

.dark {
    --resized-sticky-header-bg: var(--gray-800);
    --resized-sticky-cell-bg: var(--gray-800);
    --resized-sticky-cell-bg-emphasis: var(--gray-700);
}

Then rebuild the theme — npm run build, or npm run dev while working. The theme is a Vite entry, so editing the file alone changes nothing until it is compiled.

The defaults live in the var() fallback chain, not in a competing declaration, so your values win regardless of stylesheet order. Narrower scopes work the same way when you only want some tables restyled:

/* one resource — the page wrapper carries .fi-resource-{slug} */
.fi-resource-users {
    --resized-sticky-cell-bg: #eef2ff;
}

/* every table container in the panel */
.fi-ta-ctn {
    --resized-sticky-cell-bg: #eef2ff;
}

Unset variables keep the Filament defaults, so overriding only light mode (or only the header) is fine. Use opaque colours: a translucent value lets the scrolled columns show through the pinned cell.

Persistence

The user's pinned selection is persisted per user (session + database) alongside widths and order — no extra migration beyond the table_settings table.