Skip to main content

Introduction

Filament provides a powerful foundation for creating custom form fields. By extending the base Field class, you can build reusable field components that integrate seamlessly with Filament’s form system, including validation, state management, and Livewire integration.

Understanding the Field base class

All form fields in Filament extend the Field class located at packages/forms/src/Components/Field.php. This base class provides:
  • State management and binding
  • Validation integration
  • Label, helper text, and hint functionality
  • Required field marking
  • Autofocus capabilities
  • Integration with Filament’s schema system

Creating a basic custom field

1
Step 1: Create the field class
2
Create a new class that extends Filament\Forms\Components\Field:
3
use Filament\Forms\Components\Field;

class ColorPicker extends Field
{
    protected string $view = 'filament.forms.components.color-picker';
}
4
Step 2: Create the Blade view
5
Create the corresponding Blade view at resources/views/filament/forms/components/color-picker.blade.php:
6
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
    <input
        type="color"
        {{ $getExtraInputAttributeBag()->class(['fi-input']) }}
        {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
        @if ($isDisabled())
            disabled
        @endif
    />
</x-dynamic-component>
7
Step 3: Use your custom field
8
You can now use your field in any Filament form:
9
use App\Forms\Components\ColorPicker;

ColorPicker::make('primary_color')
    ->label('Primary Brand Color')
    ->required()

Adding custom methods

You can add custom configuration methods to your field using the fluent API pattern:
class ColorPicker extends Field
{
    protected string $view = 'filament.forms.components.color-picker';

    protected string | Closure | null $format = 'hex';

    protected bool | Closure $hasAlphaChannel = false;

    public function format(string | Closure | null $format): static
    {
        $this->format = $format;

        return $this;
    }

    public function withAlpha(bool | Closure $condition = true): static
    {
        $this->hasAlphaChannel = $condition;

        return $this;
    }

    public function getFormat(): ?string
    {
        return $this->evaluate($this->format);
    }

    public function hasAlphaChannel(): bool
    {
        return (bool) $this->evaluate($this->hasAlphaChannel);
    }
}

Using concerns for reusable functionality

Filament uses traits (concerns) to add reusable functionality. You can use existing concerns or create your own:
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Concerns\HasPlaceholder;
use Filament\Forms\Components\Concerns\CanBeReadOnly;

class TagInput extends Field
{
    use HasPlaceholder;
    use CanBeReadOnly;

    protected string $view = 'filament.forms.components.tag-input';

    protected int | Closure | null $maxTags = null;

    public function maxTags(int | Closure | null $count): static
    {
        $this->maxTags = $count;

        return $this;
    }

    public function getMaxTags(): ?int
    {
        return $this->evaluate($this->maxTags);
    }
}

Implementing validation

Custom fields can add validation rules using the rule() method:
class PhoneInput extends Field
{
    protected string $view = 'filament.forms.components.phone-input';

    protected string | Closure | null $countryCode = null;

    public function countryCode(string | Closure | null $code): static
    {
        $this->countryCode = $code;

        return $this;
    }

    public function getCountryCode(): ?string
    {
        return $this->evaluate($this->countryCode);
    }

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

        $this->rule('regex:/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\.\/0-9]*$/');

        $this->rule(
            static fn (PhoneInput $component): string => "min:{$component->getMinLength()}",
            static fn (PhoneInput $component): bool => filled($component->getMinLength())
        );
    }
}

Working with state casts

State casts transform data between the form and database. You can implement custom state casts:
use Filament\Schemas\Components\StateCasts\Contracts\StateCast;

class MoneyStateCast implements StateCast
{
    public function get(mixed $state): mixed
    {
        return $state ? $state / 100 : null;
    }

    public function set(mixed $state): mixed
    {
        return $state ? (int) ($state * 100) : null;
    }
}

class MoneyInput extends Field
{
    protected string $view = 'filament.forms.components.money-input';

    public function getDefaultStateCasts(): array
    {
        return [
            ...parent::getDefaultStateCasts(),
            app(MoneyStateCast::class),
        ];
    }
}

Advanced example: Rich text editor integration

Here’s a more complex example integrating a third-party JavaScript library:
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Concerns\HasPlaceholder;
use Filament\Support\Concerns\HasExtraAlpineAttributes;

class QuillEditor extends Field
{
    use HasPlaceholder;
    use HasExtraAlpineAttributes;

    protected string $view = 'filament.forms.components.quill-editor';

    protected array | Closure $toolbar = [];

    protected string | Closure | null $theme = 'snow';

    public function toolbar(array | Closure $toolbar): static
    {
        $this->toolbar = $toolbar;

        return $this;
    }

    public function theme(string | Closure | null $theme): static
    {
        $this->theme = $theme;

        return $this;
    }

    public function getToolbar(): array
    {
        return $this->evaluate($this->toolbar) ?: [
            ['bold', 'italic', 'underline'],
            ['link', 'image'],
            [['list' => 'ordered'], ['list' => 'bullet']],
        ];
    }

    public function getTheme(): ?string
    {
        return $this->evaluate($this->theme);
    }
}

Accessing the Livewire component

You can access the parent Livewire component and interact with it:
class DynamicSelect extends Field
{
    protected string $view = 'filament.forms.components.dynamic-select';

    protected Closure | null $optionsCallback = null;

    public function loadOptionsUsing(Closure $callback): static
    {
        $this->optionsCallback = $callback;

        return $this;
    }

    public function getOptions(): array
    {
        if ($this->optionsCallback === null) {
            return [];
        }

        return $this->evaluate($this->optionsCallback, [
            'livewire' => $this->getLivewire(),
            'get' => $this->makeGetUtility(),
        ]);
    }
}
Usage:
DynamicSelect::make('city')
    ->loadOptionsUsing(function (Filament\Forms\Get $get) {
        $countryId = $get('country_id');

        if (! $countryId) {
            return [];
        }

        return City::where('country_id', $countryId)
            ->pluck('name', 'id')
            ->toArray();
    })

Registering custom fields globally

You can register custom fields as macros to make them available on all field instances:
use Filament\Forms\Components\Field;

Field::macro('tooltip', function (string | Closure | null $tooltip) {
    $this->extraAttributes([
        'x-tooltip' => $tooltip,
    ]);

    return $this;
});
Now you can use it on any field:
TextInput::make('email')
    ->tooltip('Enter your email address')

Best practices

  • Always extend the Field base class for full integration
  • Use the evaluate() method to support closures in configuration
  • Follow Filament’s naming conventions: use is/should/can/has prefixes for boolean properties
  • Implement the setUp() method for default configuration
  • Use concerns (traits) for reusable functionality
  • Never use final classes - allow users to extend your fields
  • Use app() instead of new for dependency resolution to allow binding custom implementations
  • Add comprehensive PHPDoc annotations for arrays and complex types

Publishing as a package

When creating a package with custom fields:
use Illuminate\Support\ServiceProvider;

class CustomFieldsServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'custom-fields');

        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/custom-fields'),
        ], 'custom-fields-views');
    }
}
Update your field’s view path:
protected string $view = 'custom-fields::color-picker';

Build docs developers (and LLMs) love