Advanced Filament: Custom Field Plugins, Custom Columns, and Render Hooks
#filament #laravel #php #filament-plugins

Advanced Filament: Custom Field Plugins, Custom Columns, and Render Hooks

3 min read Mohamed Said Mohamed Said

Why Extend Filament at the Component Level

Filament ships with a rich set of fields and columns, but production panels inevitably need components that don't exist yet — a colour-swatch picker, a rich diff viewer, a sparkline column. The wrong move is to fork core or paste raw Blade into a resource. The right move is to build a proper plugin that can be versioned, tested, and shared.

This article covers three extension points: a custom Form field plugin, a custom Table column, and render hooks for surgical UI injection.


1. Custom Form Field Plugin

A Filament field is a class that extends Filament\Forms\Components\Field and pairs with a Blade view.

// src/Forms/Components/ColourSwatchPicker.php
namespace Acme\FilamentColour\Forms\Components;

use Filament\Forms\Components\Field;

class ColourSwatchPicker extends Field
{
    protected string $view = 'filament-colour::forms.components.colour-swatch-picker';

    /** @var array<string> */
    protected array $swatches = [];

    public function swatches(array $colours): static
    {
        $this->swatches = $colours;
        return $this;
    }

    public function getSwatches(): array
    {
        return $this->swatches;
    }
}

The Blade view receives $field automatically:

{{-- resources/views/forms/components/colour-swatch-picker.blade.php --}}
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
    <div x-data="{ selected: @entangle($getStatePath()) }" class="flex gap-2">
        @foreach ($field->getSwatches() as $colour)
            <button
                type="button"
                :class="selected === '{{ $colour }}' ? 'ring-2 ring-offset-1' : ''"
                style="background:{{ $colour }}"
                class="w-8 h-8 rounded-full"
                @click="selected = '{{ $colour }}'"
            ></button>
        @endforeach
    </div>
</x-dynamic-component>

Register it in your plugin's service provider so auto-discovery works:

public function packageBooted(): void
{
    Filament::registerRenderHook('panels::body.end', fn () => '');
    // Blade component registration happens via package view namespace
}

Usage in a resource:

ColourSwatchPicker::make('brand_colour')
    ->swatches(['#ef4444', '#3b82f6', '#22c55e'])
    ->required(),

2. Custom Table Column

Custom columns extend Filament\Tables\Columns\Column and follow the same view convention.

namespace Acme\FilamentColour\Tables\Columns;

use Filament\Tables\Columns\Column;

class ColourSwatchColumn extends Column
{
    protected string $view = 'filament-colour::tables.columns.colour-swatch-column';
}
{{-- tables/columns/colour-swatch-column.blade.php --}}
<div
    class="w-6 h-6 rounded-full border border-gray-200"
    style="background: {{ $getState() }}"
    title="{{ $getState() }}"
></div>

Because $getState() resolves through Filament's normal attribute pipeline, sorting, searching, and formatStateUsing() all work without extra effort.

ColourSwatchColumn::make('brand_colour')
    ->label('Brand')
    ->sortable(),

3. Render Hooks for Surgical UI Injection

Render hooks let you inject Blade output at named slots across every Filament panel without touching a single core file. They are registered in a service provider or panel provider.

use Filament\Support\Facades\FilamentView;
use Illuminate\Support\Facades\Blade;

FilamentView::registerRenderHook(
    'panels::topbar.end',
    fn (): string => Blade::render('<livewire:environment-badge />'),
);

Available hooks include panels::body.start, panels::sidebar.nav.start, panels::page.start, and many more — check the Filament docs for the full list per version.

For hooks that should only fire on specific pages, gate them:

use Filament\Pages\Page;

FilamentView::registerRenderHook(
    'panels::page.end',
    fn (): string => Blade::render('<livewire:audit-trail-drawer />'),
    scopes: App\Filament\Resources\OrderResource\Pages\EditOrder::class,
);

Packaging It All Together

Wrap everything in a Spatie Laravel Package Tools plugin:

public function configurePackage(Package $package): void
{
    $package
        ->name('filament-colour')
        ->hasViews()
        ->hasConfigFile();
}

Filament's own plugin interface (FilamentPlugin) lets you hook into panel registration cleanly, giving you access to the panel instance for conditional logic.


Key Takeaways

  • Extend Field or Column and pair with a namespaced Blade view — no hacks needed.
  • @entangle($getStatePath()) is the correct Alpine bridge for two-way field state.
  • Render hooks are scoped to panels and pages, keeping injection surgical and reversible.
  • Package everything with Spatie Package Tools; Filament's auto-discovery handles the rest.
  • Custom columns inherit sorting, searching, and formatStateUsing() for free.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can a custom Filament field support validation rules like built-in fields?
Yes. Because your field extends `Filament\Forms\Components\Field`, you can chain any built-in rule method (`->required()`, `->rules([])`, `->minLength()`) and Filament's form validation pipeline treats it identically to a native field.
Q02 How do render hooks differ between Filament v3 and v4?
The hook names and the registration API are largely the same, but v4 introduced additional schema-level hooks tied to the unified Schema API. Always check the version-specific hook reference; hooks added in v4 will silently do nothing in a v3 panel.
Q03 Is it safe to use Livewire components inside render hooks?
Yes, using `Blade::render('<livewire:my-component />')` inside a render hook works, but each call mounts a full Livewire component. Keep them lightweight and avoid mounting the same component on every page load if it carries heavy state.

Continue reading

More Articles

View all