Filament v3 Custom Field Plugins, Custom Columns, and Render Hooks
#filament #laravel #php #admin-panel

Filament v3 Custom Field Plugins, Custom Columns, and Render Hooks

3 min read Mohamed Said Mohamed Said

Filament ships with an impressive component library, but real-world projects inevitably demand UI that doesn't exist out of the box. This article walks through three concrete extension points: a packaged custom field, a custom table column, and render hooks — all in Filament v3.

Building a Custom Field Plugin

A custom field is a class that extends Filament\Forms\Components\Field. The minimum contract is a Blade view and a static make() constructor.

// src/Forms/Components/ColorSwatchInput.php
namespace App\Forms\Components;

use Filament\Forms\Components\Field;

class ColorSwatchInput extends Field
{
    protected string $view = 'forms.components.color-swatch-input';

    protected array $swatches = [];

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

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

The Blade view receives $getSwatches() as a callable injected by Filament's view data layer:

{{-- resources/views/forms/components/color-swatch-input.blade.php --}}
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
    <div class="flex gap-2">
        @foreach ($getSwatches() as $color)
            <button
                type="button"
                wire:click="$set('{{ $getStatePath() }}', '{{ $color }}')"
                class="w-8 h-8 rounded-full border-2"
                style="background-color: {{ $color }}"
            ></button>
        @endforeach
    </div>
</x-dynamic-component>

Usage in a resource form:

ColorSwatchInput::make('brand_color')
    ->swatches(['#FF5733', '#33FF57', '#3357FF'])
    ->required(),

To ship this as a proper plugin, wrap it in a PluginServiceProvider that calls Filament::serving() and registers any assets or translations.

Registering a Custom Table Column

Custom columns extend Filament\Tables\Columns\Column. Override setUp() for defaults and provide a view:

namespace App\Tables\Columns;

use Filament\Tables\Columns\Column;

class BadgeListColumn extends Column
{
    protected string $view = 'tables.columns.badge-list';

    protected string $separator = ',';

    public function separator(string $separator): static
    {
        $this->separator = $separator;
        return $this;
    }

    public function getSeparator(): string
    {
        return $this->separator;
    }
}
{{-- resources/views/tables/columns/badge-list.blade.php --}}
<div class="flex flex-wrap gap-1">
    @foreach (explode($getSeparator(), $getState() ?? '') as $item)
        <span class="px-2 py-0.5 text-xs rounded-full bg-primary-100 text-primary-800">
            {{ trim($item) }}
        </span>
    @endforeach
</div>

Drop it into any table:

BadgeListColumn::make('tags')
    ->separator(',')
    ->searchable(),

Because it extends Column, sorting, searching, and formatStateUsing() all work without extra effort.

Injecting UI with Render Hooks

Render hooks let you inject HTML at named slots across the Filament shell without overriding Blade layouts. Register them in a service provider:

use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;

public function boot(): void
{
    FilamentView::registerRenderHook(
        PanelsRenderHook::BODY_START,
        fn (): string => view('partials.environment-banner')->render(),
    );
}

Scope a hook to specific pages to avoid polluting every screen:

FilamentView::registerRenderHook(
    PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABLE_BEFORE,
    fn (): string => view('partials.bulk-import-notice')->render(),
    scopes: App\Filament\Resources\OrderResource\Pages\ListOrders::class,
);

Available constants live in PanelsRenderHook, TablesRenderHook, and FormsRenderHook. Check the Filament source for the full list — it grows with each minor release.

Key Takeaways

  • Custom fields extend Field; provide a view and fluent setters — Filament handles state binding automatically.
  • Custom columns extend Column; built-in features like sorting and formatStateUsing() are inherited for free.
  • Render hooks are the clean alternative to Blade overrides; scope them to specific page classes to keep side-effects contained.
  • Package your components behind a PluginServiceProvider for reuse across projects without copy-pasting views.
  • Prefer $getStatePath() over hardcoded model keys in field views to stay compatible with nested repeaters and array state.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can a custom Filament field participate in validation like built-in fields?
Yes. Because your field extends `Field`, you can chain any standard Laravel validation rules via `->rules()`, `->required()`, or `->minLength()`. Filament's form validation pipeline treats custom fields identically to built-in ones.
Q02 How do I pass JavaScript behaviour to a custom field without breaking Livewire?
Use Alpine.js directives directly in your Blade view. Filament's Livewire integration is Alpine-aware, so `x-data`, `x-on`, and `$wire` all work inside custom field views. Avoid raw `addEventListener` calls that fire before Alpine initialises.
Q03 Are render hooks re-evaluated on every Livewire re-render?
Yes — the closure is called each time the Livewire component re-renders. Keep hook closures cheap: return a pre-rendered string or cache the view output if the content is static.

Continue reading

More Articles

View all