Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Actionable Column Laravel Package

shreejan/actionable-column

Add interactive action buttons to Filament table columns: badges or plain text with connected action buttons, custom icons and colors, clickable columns, and empty-state "+ Add" support. Works with any Filament Action and Filament v4/v5.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, then immediately check what resolved.

composer require shreejan/actionable-column

The package declares ^4.0|^5.0 for Filament, so it should slot into an existing Filament v4 or v5 app without pulling a second major. Confirm that in composer.lock before you commit. If Composer tries to upgrade Filament, pin: composer require shreejan/actionable-column:^1.1 — v1.1.0 was the release that added the v5 constraint; v1.0.x is effectively v4-only.

There is nothing else to install. No service provider to register, no migration, no vendor:publish step required for basic use. config/actionable-column.php exists only to override the custom-CSS path. That is the entire integration surface — which is the main reason this package is worth reading in full before you use it.

Where to look first, in this order:

  1. The README's Configuration Options table — it is the complete public API. Roughly eight methods; you can absorb the whole thing in two minutes and there is no hidden surface beyond it.
  2. src/Tables/Columns/ActionableColumn.php — confirm it extends TextColumn (it does) and read tapAction() to see how Action|Closure is stored and evaluated. That one method is the package.
  3. resources/views/ — the Blade view is the badge/button markup. You will be reading this file when the connected badge border is off by a pixel, and copying it if you ever need to fork. Fifteen minutes here saves an hour of CSS guessing later.

First use case: a status column on a moderation queue. The canonical integration is a column whose current value is the affordance to change it:

use Shreejan\ActionableColumn\Tables\Columns\ActionableColumn;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Support\Icons\Heroicon;

ActionableColumn::make('status')
    ->badge()
    ->color('warning')
    ->actionIcon(Heroicon::PencilSquare)
    ->actionIconColor('gray')
    ->tapAction(
        Action::make('changeStatus')
            ->label('Change Status')
            ->schema([
                Select::make('status')->options([
                    'pending' => 'Pending', 'approved' => 'Approved', 'rejected' => 'Rejected',
                ])->required(),
            ])
            ->fillForm(fn ($record) => ['status' => $record->status])
            ->action(fn ($record, array $data) => $record->update($data))
    )
    ->searchable()
    ->sortable();

Ship that on one table. If the modal opens pre-filled, the record updates, and the table refreshes, you have validated the entire contract. Everything else is a variation on those eight methods.

Implementation Patterns

The state column: badge + modal form

This is ~80% of real usage. The column is the state, the action is the transition. The highest-leverage detail is putting ->visible() on the Action:

->tapAction(
    Action::make('approve')
        ->requiresConfirmation()
        ->visible(fn ($record) => $record->approval_status === 'pending')
        ->action(fn ($record) => $record->update(['approval_status' => 'approved', 'approved_by' => auth()->id()]))
)

The badge still renders on every row; the button only appears where action is possible. Approval queues stay scannable and users stop clicking dead icons.

Empty state as an inline create path

The "+ Add" empty-state button is the most underrated feature — any column that is legitimately null on fresh records gets a one-click create affordance:

ActionableColumn::make('assigned_to')
    ->badge()
    ->actionLabel('Assign')
    ->tapAction(Action::make('assign')
        ->schema([Select::make('user_id')->relationship('assignee', 'name')->required()])
        ->action(fn ($record, array $data) => $record->update($data)));

You get a create path with zero extra UI, inline in the row where the gap is visible.

Side actions where the user already is

Not every actionable column is a state machine. "Resend invite", "clone", "archive" read very naturally here, because the user is already looking at the email/subject/title:

ActionableColumn::make('email')
    ->actionIcon(Heroicon::PaperAirplane)
    ->actionIconColor('info')
    ->tapAction(Action::make('resendInvite')
        ->requiresConfirmation()
        ->visible(fn ($record) => $record->invited_at && ! $record->email_verified_at)
        ->action(fn ($record) => $record->sendInvite()))
    ->copyable();

Everything from TextColumn still applies — copyable(), limit(), formatStateUsing(), date(), searchable(), sortable(). The correct mental model is "a TextColumn with one extra method." Layer standard column config on top exactly as you already do.

Reuse the same Action for row actions and the column

Do not define the action twice. Extract it to a static method on the resource (or a dedicated action class) and attach the same factory to both:

public static function approveAction(): Action
{
    return Action::make('approve')
        ->requiresConfirmation()
        ->authorize('approve')
        ->action(fn (Post $record) => $record->approve());
}

This is where the package earns its keep: authorization policies, modal schemas, notifications, and audit logging all carry over untouched, because it is Filament's own Action pipeline. Nothing to re-implement and nothing to keep in sync when the policy changes.

tapAction with a closure

Useful when the action depends on record type or user role:

->tapAction(fn ($record) => $record->type === 'invoice'
    ? static::approveInvoiceAction()
    : static::approveExpenseAction())

Have the closure return an Action (do not invoke ->call() or similar). Verify the return-type contract in ActionableColumn.php and confirm what happens when a record returns null — test one row deliberately.

Full-cell click for dense tables

->badge()->color('danger')->clickableColumn()->showActionIcon(false)

The badge becomes the button. Great on wide tables, risky on narrow ones — see the a11y note below.

Styling that survives composer update

Never edit public/css/. Drop overrides in resources/css/actionable-column-custom.css (auto-detected, loaded after the package defaults) or point ACTIONABLE_COLUMN_CUSTOM_CSS_PATH at a custom file. This was the entire point of v1.0.1; if you edit published assets you will silently lose the work on the next deploy.

Gotchas and Tips

The empty-state button always renders. ->showActionIcon(false) hides the icon action, not the "+ Add" button — there is no hideEmptyButton(). If a column must never permit creation, do not attach an action that permits it. This bites people who add a column "just to format the state" and discover a create path they did not design.

Closures for color() are worth verifying. The README documents color(string). Filament columns normally accept closures, but confirm against the source before you build a status map on it; the safe fallback is a static color plus formatStateUsing()/badge-class logic.

Table refresh is implicit, related data is not. $record->update() reflects after the action completes because Filament reloads the table. If your action mutates other records that are visible in the same table row (counters, related badges), assert the refresh in a browser rather than assuming.

Beware action-name collisions. Filament registers actions per Livewire component by name. If you attach Action::make('approve') as a row action and a column tap action in the same resource table, verify behaviour — the safest habit is distinct names (approveRow / approveColumn) or reusing a single shared instance deliberately, not accidentally.

Watch N+1 in visible() and color() closures. fn ($record) => $record->assignee->name inside a table column fires per row. Add ->modifyQueryUsing(fn ($query) => $query->with('assignee')) on the table. The action pipeline makes this easy to miss because closures look free.

Authorization is the Action's job, and it works. ->authorize('approve'), ->hidden(fn () => ! auth()->user()->isAdmin()), policy checks via can() — all honoured, because this is a real Filament Action. Do not hand-roll permission checks in visible(); use the same policy you use everywhere else, so the column and the row action can never disagree.

Accessibility: an icon-less clickable cell has no affordance. With clickableColumn() + showActionIcon(false), keyboard users get no visible target and screen readers get no label. Keep ->tooltip() on the Action, and prefer keeping the icon visible on tables that are not extremely dense. Confirm the cell is reachable by tab.

Testing. Filament's callTableAction() helpers are designed around table actions, not column-embedded ones; do not assume they reach a tapAction column action. The pragmatic approach: put the real logic in an invokable action class or a service method, unit test that, and cover the column wiring with a Livewire render assertion. Verify the helper behaviour in your Filament version before writing a suite around it.

Small package, zero dependents. 16 stars and no dependents means you are the QA. Read the Blade view, keep the composer constraint at ^1.1, and vendor-fork if you need markup changes. The upside: the entire plugin is small enough to audit in an afternoon, and its extension point (the custom CSS file) is a deliberate, documented escape hatch rather than an afterthought.

Namespace vs. package name. The vendor is shreejan, the namespace is Shreejan\ActionableColumn. Import it once with use at the top of the resource file and forget it — fighting IDE autocomplete in the middle of a column definition is the most common five-minute loss with this package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource