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 column, a money-input field with currency selection, a sidebar widget injected via a render hook. Reaching for a raw Blade view every time creates drift. Building a proper plugin — even an internal one — gives you auto-discovery, configuration, and testability.
Building a Custom Field Plugin
A Filament field is a PHP class that extends Filament\Forms\Components\Field and pairs with a Blade view. The plugin wrapper registers it cleanly.
// src/Fields/MoneyInput.php
namespace Acme\FilamentMoney\Fields;
use Filament\Forms\Components\Field;
class MoneyInput extends Field
{
protected string $view = 'filament-money::fields.money-input';
protected string $currency = 'USD';
public function currency(string $currency): static
{
$this->currency = $currency;
return $this;
}
public function getCurrency(): string
{
return $this->currency;
}
}
{{-- resources/views/fields/money-input.blade.php --}}
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-500">{{ $getRecord()?->currency ?? $getCurrency() }}</span>
<input
x-bind:value="$wire.entangle('{{ $getStatePath() }}')"
type="number"
step="0.01"
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
class="block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
</x-dynamic-component>
The $applyStateBindingModifiers() helper respects lazy, debounce, and live modifiers set by the form author — always use it instead of hardcoding wire:model.
Service Provider and Auto-Discovery
namespace Acme\FilamentMoney;
use Filament\Support\Assets\Css;
use Filament\Support\Facades\FilamentAsset;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
class FilamentMoneyServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package->name('filament-money')->hasViews('filament-money');
}
public function packageBooted(): void
{
FilamentAsset::register([
Css::make('filament-money', __DIR__.'/../dist/filament-money.css'),
], 'acme/filament-money');
}
}
Filament's FilamentAsset facade handles asset versioning and panel-scoped injection — no manual Vite config needed in the consuming app.
Custom Table Columns
Custom columns extend Filament\Tables\Columns\Column. The key is overriding getState() when you need derived data, and providing a typed view.
namespace Acme\FilamentMoney\Columns;
use Filament\Tables\Columns\Column;
class MoneyColumn extends Column
{
protected string $view = 'filament-money::columns.money-column';
protected string $currency = 'USD';
public function currency(string $currency): static
{
$this->currency = $currency;
return $this;
}
public function getFormattedState(): string
{
$amount = $this->getState();
if ($amount === null) return '—';
return number_format((float) $amount / 100, 2) . ' ' . $this->currency;
}
}
{{-- resources/views/columns/money-column.blade.php --}}
<div class="px-4 py-2 text-right tabular-nums">
{{ $getFormattedState() }}
</div>
Keep column views stateless — no wire:model, no Alpine state. Columns render inside a Livewire table loop; any reactive state belongs in a custom action or a slide-over, not the cell itself.
Render Hooks: Injecting Into Panel Layouts
Render hooks let you inject Blade content at named positions in any Filament panel without overriding core views.
// In a PanelProvider or plugin's register() method
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
FilamentView::registerRenderHook(
PanelsRenderHook::SIDEBAR_NAV_END,
fn (): string => view('filament-money::hooks.upgrade-banner')->render(),
);
Scope hooks to specific panels to avoid bleed across multi-panel setups:
FilamentView::registerRenderHook(
PanelsRenderHook::PAGE_HEADER_ACTIONS_BEFORE,
fn (): string => view('partials.environment-badge')->render(),
scopes: \App\Filament\AdminPanelProvider::class,
);
The scopes parameter accepts a single class string or an array — essential when you run separate admin and app panels in the same installation.
Key Takeaways
- Extend
FieldandColumndirectly; pair each with a dedicated Blade view that uses Filament's helper methods ($getStatePath(),$applyStateBindingModifiers()). - Register assets via
FilamentAsset— it handles versioning and panel scoping automatically. - Keep column views stateless; reactive behaviour belongs in actions or slide-overs.
- Use
PanelsRenderHookconstants (not raw strings) and always scope hooks to the target panel in multi-panel apps. - Even internal plugins deserve a
ServiceProvider— it makes testing, versioning, and team onboarding dramatically easier.