Filament v3 Custom Form Fields: Building a Reusable Colour-Swatch Picker
#filament #laravel #livewire #custom-fields

Filament v3 Custom Form Fields: Building a Reusable Colour-Swatch Picker

1 min read Mohamed Said Mohamed Said

Why Roll Your Own Filament Field?

Filament v3 ships with a rich field library, but product teams inevitably hit requirements that fall outside it — a star-rating input, a drag-rank list, or in our case a colour-swatch picker where users choose from a curated brand palette rather than an arbitrary hex string. Building this as a proper Field subclass (not a raw Blade hack) means you get schema-level validation, ->disabled(), ->live(), ->hint(), and every other Filament modifier for free.


Scaffolding the Field Class

Filament custom fields extend Filament\Forms\Components\Field. The minimum surface area is:

<?php

namespace App\Forms\Components;

use Filament\Forms\Components\Field;

class ColourSwatchPicker extends Field
{
    protected string $view = 'forms.components.colour-swatch-picker';

    /** @var array<string, string> label => hex */
    protected array $swatches = [];

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

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

Keep the class thin. Business logic lives in the view component; the field class is just a typed configuration bag.


The Blade View

Filament field views receive $field (the component instance) and $state (current wire model value). Use Alpine for the click interaction — no extra Livewire round-trips needed for UI feedback.

<x-dynamic-component :component="$field->getFieldWrapperView()" :field="$field">
    <div
        x-data="{ selected: @entangle($attributes->wire('model')) }"
        class="flex flex-wrap gap-3"
    >
        @foreach ($field->getSwatches() as $label => $hex)
            <button
                type="button"
                :class="selected === '{{ $hex }}'
                    ? 'ring-2 ring-offset-2 ring-primary-500 scale-110'
                    : 'opacity-70 hover:opacity-100'"
                class="w-8 h-8 rounded-full transition-all"
                style="background-color: {{ $hex }}"
                x-on:click="selected = '{{ $hex }}'"
                title="{{ $label }}"
            ></button>
        @endforeach
    </div>
</x-dynamic-component>

@entangle binds the Alpine selected variable directly to the Livewire wire model, so Filament's state management — including ->live() reactive updates — works without any custom JavaScript.


Registering and Using the Field

use App\Forms\Components\ColourSwatchPicker;

ColourSwatchPicker::make('brand_colour')
    ->label('Brand Colour')
    ->swatches([
        'Midnight' => '#1a1a2e',
        'Crimson'  => '#e63946',
        'Sage'     => '#606c38',
        'Sand'     => '#dda15e',
    ])
    ->required()
    ->live()
    ->afterStateUpdated(fn ($state, Set $set) =>
        $set('preview_hex', $state)
    ),

Because we extend Field, ->required() wires straight into Filament's validation pipeline — no custom rules() override needed for basic presence checks.


Adding a Custom Validation Rule

If you want to restrict values to the declared palette:

public function rules(mixed $component): array
{
    return [
        function (string $attribute, mixed $value, Closure $fail) use ($component) {
            if (! in_array($value, $component->getSwatches(), strict: true)) {
                $fail('Please select a colour from the palette.');
            }
        },
    ];
}

Override rules() on the field class and return an array of Laravel rule objects or closures — Filament merges them with its own validation pass automatically.


Dark-Mode Consideration

The ring-primary-500 Tailwind class already respects Filament's dark-mode token system. If your swatches include very light colours, add a conditional border:

:class="selected === '{{ $hex }}'
    ? 'ring-2 ring-offset-2 ring-primary-500'
    : 'border border-gray-200 dark:border-gray-700'"

Takeaways

  • Extend Filament\Forms\Components\Field — not a raw Livewire component — to inherit the full modifier API.
  • Use @entangle in the Blade view to bind Alpine state to Livewire without custom JS.
  • Keep the PHP class as a typed config bag; put rendering logic in the view.
  • Override rules() to add palette-restriction validation that integrates with Filament's pipeline.
  • Dark-mode support is free when you use Filament's Tailwind design tokens.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use this custom field inside a Filament Table action modal?
Yes. Because it extends Field, it works anywhere Filament renders a form schema — resource pages, action modals, and infolist panels alike.
Q02 How do I persist the hex value to the database?
Filament calls the standard Eloquent fill cycle. As long as your model has a `brand_colour` (or equivalent) fillable attribute, the value is saved automatically when the form is submitted.
Q03 Does @entangle cause extra Livewire network requests on every swatch click?
Only if you add ->live() to the field. Without it, @entangle defers the sync to the next form submission, keeping interactions snappy.

Continue reading

More Articles

View all