Why Build a Custom Filament Field Plugin?
Filament ships with a rich field library, but real-world SaaS products inevitably need domain-specific inputs — signature pads, rich colour pickers, geo-coordinate selectors. Understanding how to build one correctly means you can extend Filament without fighting its internals.
This article walks through a signature pad field: a canvas-based input that stores a base64 PNG, wired cleanly through Livewire and castable to a value object on the Eloquent model.
The Component Skeleton
Filament custom fields extend Filament\Forms\Components\Field. The minimum surface area is:
namespace Acme\SignaturePad;
use Filament\Forms\Components\Field;
class SignaturePad extends Field
{
protected string $view = 'signature-pad::signature-pad';
public function getDefaultState(): mixed
{
return null;
}
}
Register the view namespace in your service provider:
public function boot(): void
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'signature-pad');
}
Alpine.js + Livewire Entanglement
The tricky part is syncing canvas data back to Livewire's state. Filament uses $wire.entangle under the hood for its own fields, and you should too.
{{-- resources/views/signature-pad.blade.php --}}
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<div
x-data="signaturePad(@entangle($getStatePath()))"
x-init="init()"
>
<canvas
x-ref="canvas"
class="w-full border rounded-lg"
height="200"
></canvas>
<button type="button" x-on:click="clear()"
class="mt-2 text-sm text-danger-600">
Clear
</button>
</div>
</x-dynamic-component>
The Alpine component:
Alpine.data('signaturePad', (state) => ({
state,
pad: null,
init() {
const canvas = this.$refs.canvas;
this.pad = new SignaturePad(canvas); // signature_pad npm package
if (this.state) {
this.pad.fromDataURL(this.state);
}
this.pad.addEventListener('endStroke', () => {
this.state = this.pad.toDataURL();
});
},
clear() {
this.pad.clear();
this.state = null;
},
}));
The @entangle($getStatePath()) call binds the Alpine state property directly to Filament's Livewire component state path — no custom events needed.
PHP-Side: Validation and Casting
Add a validation rule to reject empty submissions when the field is required:
public function getValidationRules(): array
{
return array_merge(parent::getValidationRules(), [
$this->getStatePath() => [
fn () => function (string $attribute, mixed $value, \Closure $fail) {
if ($this->isRequired() && blank($value)) {
$fail('A signature is required.');
}
if ($value && !str_starts_with($value, 'data:image/png;base64,')) {
$fail('Invalid signature format.');
}
},
],
]);
}
On the model, a custom cast keeps the domain layer clean:
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class SignatureCast implements CastsAttributes
{
public function get($model, string $key, mixed $value, array $attributes): ?Signature
{
return $value ? new Signature($value) : null;
}
public function set($model, string $key, mixed $value, array $attributes): array
{
return [$key => $value instanceof Signature ? $value->dataUrl : $value];
}
}
// On the Eloquent model
protected $casts = [
'signature' => SignatureCast::class,
];
Package Auto-Discovery
For distribution, declare the service provider in composer.json:
"extra": {
"laravel": {
"providers": [
"Acme\\SignaturePad\\SignaturePadServiceProvider"
]
}
}
Publish assets via the service provider:
public function boot(): void
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'signature-pad');
$this->publishes([
__DIR__.'/../resources/dist' => public_path('vendor/signature-pad'),
], 'signature-pad-assets');
}
Users run php artisan vendor:publish --tag=signature-pad-assets once, then reference the JS in their Filament panel's renderHook or vite.config.js.
Takeaways
- Extend
Field, point to a Blade view, and let@entangle($getStatePath())handle Livewire sync — no custom events. - Keep validation inside
getValidationRules()so Filament's error display works automatically. - Use a dedicated Eloquent cast to keep raw base64 out of your domain objects.
- Publish JS assets separately from views so consumers can version them independently.
- Declare the service provider in
composer.jsonextras for zero-config auto-discovery.