Filament v3 Custom Field Plugins Deep Dive | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com) - [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [About](https://www.msaied.com#about)

           [  Contact](https://www.msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com)
- [ProjectsCase studies](https://www.msaied.com/projects)
- [ArticlesEngineering notes](https://www.msaied.com/articles)
- [CertificatesCredentials](https://www.msaied.com/certificates)
- [AboutHow I work](https://www.msaied.com#about)
- [ContactGet in touch](https://www.msaied.com#contact)

  [Start a conversation](https://www.msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com)
2. /
3. [Articles](https://www.msaied.com/articles)
4. /
5. Filament v3 Custom Field Plugins: Building Reusable Input Components from Scratch

 Filament v3 Custom Field Plugins: Building Reusable Input Components from Scratch
==================================================================================

 Go beyond built-in fields and build a fully packaged Filament v3 custom field plugin — covering the Field contract, state hydration, Blade views, and Alpine.js wiring for real-world reuse.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com#person) Published 17 Oct 2026 · Updated 17 Oct 2026 · 4 min read

ShareCopy linkCopied

 ![Filament v3 Custom Field Plugins: Building Reusable Input Components from Scratch](https://cdn.msaied.com/791/8bc85df3b88387f1d37d06a7c65b0de3.png) 

  On this page +1. [Why Roll Your Own Filament Field?](#why-roll-your-own-filament-field)
2. [The Field Contract](#the-field-contract)
3. [The Blade View and Alpine Wiring](#the-blade-view-and-alpine-wiring)
4. [Packaging It Properly](#packaging-it-properly)
5. [Validation Integration](#validation-integration)
6. [Takeaways](#takeaways)

 Why Roll Your Own Filament Field?
---------------------------------

Filament ships with a rich set of inputs, but production apps inevitably need something bespoke — a colour-swatch picker, a tag tokeniser backed by a custom API, or a signature pad. Reaching for a raw Livewire component inside a form schema breaks the hydration contract and loses validation integration. The right move is a proper `Field` subclass that plugs into the schema pipeline cleanly.

This article walks through building a `TagTokenizerField` that accepts a comma-separated list of strings, stores them as a JSON array, and ships as a standalone Composer package.

---

The Field Contract
------------------

Every Filament form field extends `Filament\Forms\Components\Field`. The two lifecycle hooks you almost always need are:

- **`setUp()`** — declare default state, rules, and component configuration.
- **`getState()` / `dehydrateState()`** — transform raw Livewire state before it reaches your model.

```php
namespace Acme\TagTokenizer\Forms\Components;

use Filament\Forms\Components\Field;

class TagTokenizerField extends Field
{
    protected string $view = 'tag-tokenizer::forms.components.tag-tokenizer';

    protected function setUp(): void
    {
        parent::setUp();

        $this->default([]);

        $this->dehydrateStateUsing(
            fn (?string $state): array =>
                collect(explode(',', (string) $state))
                    ->map(fn (string $t) => trim($t))
                    ->filter()
                    ->values()
                    ->all()
        );

        $this->afterStateHydrated(function (TagTokenizerField $component, mixed $state): void {
            $component->state(
                is_array($state) ? implode(', ', $state) : (string) $state
            );
        });
    }
}

```

`dehydrateStateUsing` runs just before the form saves, converting the comma string back to an array. `afterStateHydrated` runs when the form loads, converting the stored array into a display string.

---

The Blade View and Alpine Wiring
--------------------------------

Filament passes `$getStatePath()` to every field view. Use it as the Alpine `x-model` target so Livewire's morphing stays in sync.

```blade
{{-- resources/views/forms/components/tag-tokenizer.blade.php --}}

                    ×

```

```javascript
// resources/js/tag-tokenizer.js
document.addEventListener('alpine:init', () => {
    Alpine.data('tagTokenizer', (initial) => ({
        tags: initial ? initial.split(',').map(t => t.trim()).filter(Boolean) : [],
        get csv() { return this.tags.join(', '); },
        add(input) {
            const val = input.value.replace(/,/g, '').trim();
            if (val) this.tags.push(val);
            input.value = '';
        },
        remove(i) { this.tags.splice(i, 1); },
    }));
});

```

The hidden `` uses `x-bind:name` with `$wire.entangle` so Livewire picks up changes without a round-trip on every keystroke — `wire:ignore` on the wrapper prevents Livewire from clobbering Alpine's DOM.

---

Packaging It Properly
---------------------

A clean service provider registers the view namespace and optionally publishes assets:

```php
class TagTokenizerServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'tag-tokenizer');

        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/tag-tokenizer'),
        ], 'tag-tokenizer-views');

        FilamentAsset::register([
            Js::make('tag-tokenizer', __DIR__.'/../resources/dist/tag-tokenizer.js'),
        ], package: 'acme/tag-tokenizer');
    }
}

```

`FilamentAsset::register` ensures the script is injected into the Filament panel head without manual `@push('scripts')` calls — it respects Filament's asset versioning and CDN configuration.

---

Validation Integration
----------------------

Because the field dehydrates to an array before validation runs, you can attach standard Laravel array rules:

```php
TagTokenizerField::make('labels')
    ->rules(['array', 'max:10'])
    ->label('Labels'),

```

For per-tag rules, add a `dehydrateStateUsing` override in the consuming app or extend the field with a `maxTagLength(int $chars)` fluent method that appends a custom rule closure.

---

Takeaways
---------

- Use `afterStateHydrated` and `dehydrateStateUsing` to own the full hydration cycle without touching your model.
- `wire:ignore` + `x-bind:name` with `$wire.entangle` is the reliable pattern for Alpine-heavy fields that must not re-render on every Livewire tick.
- `FilamentAsset::register` is the correct way to ship JS/CSS with a field plugin — avoid raw `@push`.
- Keep the Blade view thin; push all interactivity into a named `Alpine.data` component so it is testable and overridable.
- Publish views as a vendor stub so consumers can customise styling without forking the package.

- [filament](https://www.msaied.com/articles?search=filament)
- [laravel](https://www.msaied.com/articles?search=laravel)
- [livewire](https://www.msaied.com/articles?search=livewire)
- [package-development](https://www.msaied.com/articles?search=package-development)

 Frequently asked questions 
---------------------------

  Why use `wire:ignore` on the field wrapper instead of letting Livewire manage the DOM?Livewire's DOM diffing will overwrite Alpine's dynamically rendered tag chips on every re-render. `wire:ignore` tells Livewire to skip that subtree after the initial render, while the hidden input with `x-bind:name` keeps the Livewire state in sync through the entangle binding.

   Can I use this pattern for fields that need server-side search, like a remote autocomplete?Yes. Instead of a hidden input, dispatch a Livewire event from Alpine (`$dispatch('tag-search', { query })`) and handle it with a `#\[On\]` listener on the Livewire component. The field's `getSearchResultsUsing` pattern from `Select` is also worth studying as a reference implementation.

   How do I write Pest tests for a custom field's dehydration logic?Instantiate the field directly and call `-&gt;dehydrateState($state)` in a unit test, or use Filament's `livewire(CreateResource::class)-&gt;fillForm(\['labels' =&gt; 'foo, bar'\])-&gt;call('create')-&gt;assertHasNoFormErrors()` integration style to test the full round-trip.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articlePostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection](https://www.msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection-3)  

   On this page
-------------

1. [Why Roll Your Own Filament Field?](#why-roll-your-own-filament-field)
2. [The Field Contract](#the-field-contract)
3. [The Blade View and Alpine Wiring](#the-blade-view-and-alpine-wiring)
4. [Packaging It Properly](#packaging-it-properly)
5. [Validation Integration](#validation-integration)
6. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/790/3b305e067cf21c59b93a26f96786fabd.png)  · 2 min read### PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection

17 Oct 2026 ](https://www.msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection-3) [ ![](https://cdn.msaied.com/789/40fed96a77e341e8fdd2b49f888d1d92.png)  · 4 min read### Event Sourcing in Laravel: Aggregates, Projectors, and Reactors Without a Framework

17 Oct 2026 ](https://www.msaied.com/articles/event-sourcing-in-laravel-aggregates-projectors-and-reactors-without-a-framework) [ ![](https://cdn.msaied.com/788/efb3e90bd9347b097a81544cadd32b67.png)  · 4 min read### Read/Write Splitting and Sticky Reads in Laravel: A Production Guide

17 Oct 2026 ](https://www.msaied.com/articles/readwrite-splitting-and-sticky-reads-in-laravel-a-production-guide-2) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/sitemap.xml)
