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 andformatStateUsing()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
PluginServiceProviderfor 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.