---
title: Custom fields
---

## Introduction

You can create a reusable field with a Blade view, React, Vue, Svelte, or framework-free JavaScript. Start by generating a field, then customize its view or renderer. Filament handles the field wrapper, validation, and state binding.

To generate a Blade-based field:

```bash
php artisan make:filament-form-field LocationPicker
```

Use the generated class in your form's schema:

```php
use App\Filament\Forms\Components\LocationPicker;

LocationPicker::make('location')
```

Edit the generated Blade view to render your input, following [custom field classes](#custom-field-classes). For a JavaScript-rendered field, choose one of the options below instead.

### Generating a JavaScript field

Install your application's JavaScript dependencies, including Vite, before running the generator, for example with `npm install`.

Add one renderer flag to the command:

```bash
php artisan make:filament-form-field LocationPicker --react
php artisan make:filament-form-field LocationPicker --vue
php artisan make:filament-form-field LocationPicker --svelte
php artisan make:filament-form-field LocationPicker --js
```

The command creates a PHP field class and a text-input renderer in `resources/js/filament/forms/components/`, without a Blade view. It installs missing dependencies, updates supported Vite configurations, and offers to compile your assets. Use the generated class in your schema as shown above, then replace the starter input with your own UI.

For TypeScript, add `--typescript` or its alias `--ts`:

```bash
php artisan make:filament-form-field LocationPicker --react --ts
```

The [JavaScript renderer examples](#rendering-fields-with-javascript-frameworks) show how to build a location picker with each option.

#### Customizing generation

Use `--pm=yarn` to use Yarn instead of npm, `--skip-install` to install dependencies yourself, or `--skip-build` to skip the compilation prompt. Blade-based generation does not run a package manager.

The generated PHP class uses `HasJsRenderer` and loads its entry through `Vite::asset()`. The generated files depend on your chosen renderer:

- React: `location-picker.jsx`, or `location-picker.tsx` with `--typescript`.
- Vue: `location-picker.js` and `LocationPicker.vue`. With `--typescript`, the entry uses `.ts` and the component uses `<script setup lang="ts">`.
- Svelte: `location-picker.svelte.js` and `LocationPicker.svelte`. With `--typescript`, the entry uses `.svelte.ts` and the component uses `<script lang="ts">`.
- Framework-free JavaScript: `location-picker.js`, or `location-picker.ts` with `--typescript`.

The React component and Vue or Svelte import are named `LocationPicker`, matching the PHP class. Names that start with a lowercase letter or conflict with a starter's imports use the local name `Component` instead. Filenames and the default-exported `mountLocationPicker()` function still use the requested name.

The starters support React 18 or later, Vue 3.3 or later within Vue 3, and Svelte 5. Missing React installations default to React 19. Existing compatible dependencies are preserved; incompatible versions require a manual upgrade.

Nested field names, such as `Maps/LocationPicker`, place the JavaScript files in a `maps/` subdirectory. TypeScript generation also configures the `@filament/forms/js-field` [type alias](#typing-renderers).

If you configure an alternative component location, renderer directories follow the generated PHP namespace, with your application's root namespace removed and the remaining directories converted to kebab case. For example, `App\Admin\Forms\Components\LocationPicker` uses `resources/js/admin/forms/components/location-picker.js`. Namespaces outside your application's root are retained.

If your configuration cannot be updated automatically, the command prints the remaining manual steps. Follow the [Vite module setup](../advanced/assets#building-lazy-loaded-es-modules) to complete them. For a plugin, use a [published module](#building-reusable-plugin-fields) instead of the application's Vite manifest.

## Custom field classes

The Blade-based generator creates a field class that selects its view:

```php
use Filament\Forms\Components\Field;

class LocationPicker extends Field
{
    protected string $view = 'filament.forms.components.location-picker';
}
```

It will also create a view file at `resources/views/filament/forms/components/location-picker.blade.php`.

<Aside variant="info">
    Filament form fields are **not** Livewire components. Defining public properties and methods on a form field class will not make them accessible in the Blade view.
</Aside>

### Binding a field to its state

A field's state is stored in a public property on its Livewire component. Use `$getStatePath()` in the field's view to bind to the correct property, including when the field is nested inside a repeater or another component. You can bind with [`wire:model`](https://livewire.laravel.com/docs/properties#data-binding) or [entangle an Alpine.js property](https://livewire.laravel.com/docs/javascript#the-wire-object):

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    <input wire:model="{{ $getStatePath() }}" />

    <!-- Or -->

    <div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
        <input x-model="state" />
    </div>
</x-dynamic-component>
```

See [obeying state binding modifiers](#obeying-state-binding-modifiers) to support options such as `live()` and `debounce()`. Preserve the field's [accessibility associations](overview#preserving-accessibility-in-custom-field-views) when creating its control markup.

If your field uses third-party JavaScript libraries, you can [load its Alpine.js component asynchronously](../advanced/assets#asynchronous-alpinejs-components) so those assets are only loaded when needed.

### Accessing the state of another component in the Blade view

Inside the Blade view, you may access the state of another component in the schema using the `$get()` function:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    {{ $get('email') }}
</x-dynamic-component>
```

<Aside variant="tip">
    Unless a form field is [reactive](../forms/overview#the-basics-of-reactivity), the Blade view will not refresh when the value of the field changes, only when the next user interaction occurs that makes a request to the server. If you need to react to changes in a field's value, it should be `live()`.
</Aside>

### Accessing the Eloquent record in the Blade view

Inside the Blade view, you may access the current Eloquent record using the `$record` variable:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    {{ $record->name }}
</x-dynamic-component>
```

### Accessing the current operation in the Blade view

Inside the Blade view, you may access the current operation, usually `create`, `edit` or `view`, using the `$operation` variable:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    @if ($operation === 'create')
        This is a new conference.
    @else
        This is an existing conference.
    @endif
</x-dynamic-component>
```

### Accessing the current Livewire component instance in the Blade view

Inside the Blade view, you may access the current Livewire component instance using `$this`:

```blade
@php
    use Filament\Resources\Users\RelationManagers\ConferencesRelationManager;
@endphp

<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    @if ($this instanceof ConferencesRelationManager)
        You are editing conferences the of a user.
    @endif
</x-dynamic-component>
```

### Accessing the current field instance in the Blade view

Inside the Blade view, you may access the current field instance using `$field`. You can call public methods on this object to access other information that may not be available in variables:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    @if ($field->getState())
        This is a new conference.
    @endif
</x-dynamic-component>
```

### Adding a configuration method to a custom field class

You may add a public method to the custom field class that accepts a configuration value, stores it in a protected property, and returns it again from another public method:

```php
use Filament\Forms\Components\Field;

class LocationPicker extends Field
{
    protected string $view = 'filament.forms.components.location-picker';
    
    protected ?float $zoom = null;

    public function zoom(?float $zoom): static
    {
        $this->zoom = $zoom;

        return $this;
    }

    public function getZoom(): ?float
    {
        return $this->zoom;
    }
}
```

Now, in the Blade view for the custom field, you may access the zoom using the `$getZoom()` function:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    {{ $getZoom() }}
</x-dynamic-component>
```

Any public method that you define on the custom field class can be accessed in the Blade view as a variable function in this way.

To pass the configuration value to the custom field class, you may use the public method:

```php
use App\Filament\Forms\Components\LocationPicker;

LocationPicker::make('location')
    ->zoom(0.5)
```

#### Allowing utility injection in a custom field configuration method

[Utility injection](overview#field-utility-injection) is a powerful feature of Filament that allows users to configure a component using functions that can access various utilities. You can allow utility injection by ensuring that the parameter type and property type of the configuration allows the user to pass a `Closure`. In the getter method, you should pass the configuration value to the `$this->evaluate()` method, which will inject utilities into the user's function if they pass one, or return the value if it is static:

```php
use Closure;
use Filament\Forms\Components\Field;

class LocationPicker extends Field
{
    protected string $view = 'filament.forms.components.location-picker';
    
    protected float | Closure | null $zoom = null;

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

        return $this;
    }

    public function getZoom(): ?float
    {
        return $this->evaluate($this->zoom);
    }
}
```

Now, you can pass a static value or a function to the `zoom()` method, and [inject any utility](overview#field-utility-injection) as a parameter:

```php
use App\Filament\Forms\Components\LocationPicker;

LocationPicker::make('location')
    ->zoom(fn (Conference $record): float => $record->isGlobal() ? 1 : 0.5)
```

### Obeying state binding modifiers

When you bind a field to a state path, you may use the `defer` modifier to ensure that the state is only sent to the server when the user submits the form, or whenever the next Livewire request is made. This is the default behavior.

However, you may use the [`live()`](overview#the-basics-of-reactivity) on a field to ensure that the state is sent to the server immediately when the user interacts with the field. This allows for lots of advanced use cases as explained in the [reactivity](overview#the-basics-of-reactivity) section of the documentation.

Filament provides a `$applyStateBindingModifiers()` function that you may use in your view to apply any state binding modifiers to a `wire:model` or `$wire.$entangle()` binding:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    <input {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}" />

    <!-- Or -->

    <div x-data="{ state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$getStatePath()}')") }} }">
        <input x-model="state" />
    </div>
</x-dynamic-component>
```

## Rendering fields with JavaScript frameworks

After [generating a JavaScript field](#generating-a-javascript-field), customize its renderer using one of the examples below. Your application owns the renderer and its dependencies; Filament does not bundle a framework.

You can also use `JsField` directly without generating a PHP class. Pass the URL of your renderer's ES module to `renderer()`:

```php
use Filament\Forms\Components\JsField;
use Illuminate\Support\Facades\Vite;

JsField::make('location')
    ->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
    ->default(['latitude' => 51.5, 'longitude' => -0.12])
```

<Aside variant="danger">
    Only use trusted application URLs or `RawJs` code; never let user input choose executable modules or interpolate it into renderer expressions. Renderers are not sandboxed and must comply with your application's CSP and cross-origin policy. Validate and authorize submitted values in PHP, including for disabled or read-only fields.
</Aside>

Filament loads the module when the field initializes, without a global registry or a script tag. Each field gets its own mounted instance, even when the browser has already loaded the module for another field.

<UtilityInjection set="formFields" version="5.x">As well as allowing a static value, the `renderer()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>

### Writing a renderer

Each renderer exports a mount function that receives `host`, `props`, and optional schema `utilities`. Render inside `host`, then return `update(props)` to receive changes and `destroy()` to clean up. Filament supplies state, accessibility props, and callbacks to connect your input to the form.

Choose one example below. If you used the generator, adapt it in your generated files and keep their existing entry paths. If you create the files manually, follow the [Vite module setup](../advanced/assets#building-lazy-loaded-es-modules) to compile the example's entry file.

#### Creating a React field

Create `resources/js/fields/location-picker.jsx`. The component edits a latitude and longitude, storing an empty input as `null`. Apply Filament's input props so labels, descriptions, validation, and disabled or read-only states work:

```jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

function LocationPicker(props) {
    return ['latitude', 'longitude'].map((coordinate) => (
        <label key={coordinate}>
            {coordinate === 'latitude' ? 'Latitude' : 'Longitude'}
            <input
                id={coordinate === 'latitude' ? props.id : `${props.id}-longitude`}
                type="number"
                step="any"
                value={props.value?.[coordinate] ?? ''}
                disabled={props.isDisabled}
                readOnly={props.isReadOnly}
                required={props.isRequired}
                aria-invalid={props.isInvalid}
                aria-describedby={props.ariaDescribedBy}
                onChange={(event) => props.onChange({
                    ...props.value,
                    [coordinate]: event.target.value === '' ? null : Number(event.target.value),
                })}
                onBlur={props.onBlur}
            />
        </label>
    ))
}

export default function mountLocationPicker({ host, props: initialProps }) {
    const root = createRoot(host)
    const update = (props) => root.render(<LocationPicker {...props} />)

    update(initialProps)

    return { update, destroy: () => root.unmount() }
}
```

#### Creating a Vue field

Create `resources/js/fields/LocationPicker.vue`:

```vue
<script setup>
defineOptions({ inheritAttrs: false })

const props = defineProps(['value', 'id', 'isDisabled', 'isReadOnly', 'isRequired', 'isInvalid', 'ariaDescribedBy', 'onChange', 'onBlur'])

function changeCoordinate(coordinate, event) {
    props.onChange({
        ...props.value,
        [coordinate]: event.target.value === '' ? null : Number(event.target.value),
    })
}
</script>

<template>
    <label v-for="coordinate in ['latitude', 'longitude']" :key="coordinate">
        {{ coordinate === 'latitude' ? 'Latitude' : 'Longitude' }}
        <input
            :id="coordinate === 'latitude' ? props.id : `${props.id}-longitude`"
            type="number"
            step="any"
            :value="props.value?.[coordinate] ?? ''"
            :disabled="props.isDisabled"
            :readonly="props.isReadOnly"
            :required="props.isRequired"
            :aria-invalid="props.isInvalid"
            :aria-describedby="props.ariaDescribedBy"
            @input="changeCoordinate(coordinate, $event)"
            @blur="props.onBlur()"
        />
    </label>
</template>
```

Create `resources/js/fields/location-picker.js` to mount it. Use this entry's URL in `renderer()`:

```js
import { createApp, h, shallowRef } from 'vue'
import LocationPicker from './LocationPicker.vue'

export default function mountLocationPicker({ host, props: initialProps }) {
    const props = shallowRef(initialProps)
    const application = createApp({
        setup: () => () => h(LocationPicker, props.value),
    })

    application.mount(host)

    return {
        update(nextProps) {
            props.value = nextProps
        },
        destroy: () => application.unmount(),
    }
}
```

#### Creating a Svelte field

Create `resources/js/fields/LocationPicker.svelte`:

```svelte
<script>
    let { value, id, isDisabled, isReadOnly, isRequired, isInvalid, ariaDescribedBy, onChange, onBlur } = $props()

    function changeCoordinate(coordinate, event) {
        onChange({
            ...value,
            [coordinate]: event.target.value === '' ? null : Number(event.target.value),
        })
    }
</script>

{#each ['latitude', 'longitude'] as coordinate (coordinate)}
    <label>
        {coordinate === 'latitude' ? 'Latitude' : 'Longitude'}
        <input
            id={coordinate === 'latitude' ? id : `${id}-longitude`}
            type="number"
            step="any"
            value={value?.[coordinate] ?? ''}
            disabled={isDisabled}
            readonly={isReadOnly}
            required={isRequired}
            aria-invalid={isInvalid}
            aria-describedby={ariaDescribedBy}
            oninput={(event) => changeCoordinate(coordinate, event)}
            onblur={onBlur}
        />
    </label>
{/each}
```

Create `resources/js/fields/location-picker.svelte.js` to mount it. The `.svelte.js` extension allows the Svelte compiler to process `$state`. Use this entry's URL in `renderer()`:

```js
import { mount, unmount } from 'svelte'
import LocationPicker from './LocationPicker.svelte'

export default function mountLocationPicker({ host, props: initialProps }) {
    const props = $state({ ...initialProps })
    const component = mount(LocationPicker, { target: host, props })

    return {
        update: (nextProps) => Object.assign(props, nextProps),
        destroy: () => unmount(component),
    }
}
```

#### Creating a framework-free JavaScript field

You do not need a framework to build a JavaScript field. Create `resources/js/fields/location-picker.js`, add it to your Vite input, and use its built URL in `renderer()` as shown above. The following renderer creates the same latitude and longitude inputs using browser DOM APIs:

```js
export default function mountLocationPicker({ host, props: initialProps }) {
    const coordinates = ['latitude', 'longitude']
    let currentProps = initialProps
    const inputs = Object.fromEntries(coordinates.map((coordinate) => {
        const label = document.createElement('label')
        const labelText = document.createTextNode(
            coordinate === 'latitude' ? 'Latitude' : 'Longitude',
        )
        const input = document.createElement('input')

        input.type = 'number'
        input.step = 'any'
        label.append(labelText, input)
        host.append(label)

        return [coordinate, input]
    }))

    const onInput = (event) => {
        const coordinate = coordinates.find(
            (coordinate) => inputs[coordinate] === event.currentTarget,
        )

        initialProps.onChange({
            ...currentProps.value,
            [coordinate]: event.currentTarget.value === ''
                ? null
                : Number(event.currentTarget.value),
        })
    }

    for (const input of Object.values(inputs)) {
        input.addEventListener('input', onInput)
        input.addEventListener('blur', initialProps.onBlur)
    }

    const update = (props) => {
        currentProps = props

        coordinates.forEach((coordinate) => {
            const input = inputs[coordinate]

            input.id = coordinate === 'latitude' ? currentProps.id : `${currentProps.id}-longitude`
            input.value = currentProps.value?.[coordinate] ?? ''
            input.disabled = currentProps.isDisabled
            input.readOnly = currentProps.isReadOnly
            input.required = currentProps.isRequired
            input.setAttribute('aria-invalid', String(currentProps.isInvalid))
            input.setAttribute('aria-describedby', currentProps.ariaDescribedBy)
        })
    }

    update(initialProps)

    return {
        update,
        destroy() {
            for (const input of Object.values(inputs)) {
                input.removeEventListener('input', onInput)
                input.removeEventListener('blur', initialProps.onBlur)
            }

            host.replaceChildren()
        },
    }
}
```

The examples leave styling to your application. See [registering CSS files](../advanced/assets#registering-css-files) to include a stylesheet with your field.

### Receiving state and emitting changes

Use `props.value` to render the field's current state and call `props.onChange(nextValue)` after a user interaction. Always pass the complete value, rather than mutating the incoming object. Values must be JSON-serializable: `null`, scalars, arrays, or objects. Initialize a particular shape using `default()`, and handle cleared values in your component.

Filament supplies the complete props to both the mount function and `update(props)`, including `onChange()` and `onBlur()`. These callbacks keep the same function references for the mounted field's lifetime, so you can replace props on each update or retain the callbacks for native event listeners. Do not call `onChange()` when receiving props or rendering the component, as this can create feedback loops.

By default, edits update local Livewire state and are sent on the next request. Use the usual [state binding modifiers](#obeying-state-binding-modifiers) to change this behavior:

```php
use Filament\Forms\Components\JsField;
use Illuminate\Support\Facades\Vite;

JsField::make('location')
    ->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
    ->live(onBlur: true)
```

`live()` sends changes immediately, `live(onBlur: true)` waits for `props.onBlur()`, and `live(debounce: 500)` waits after the latest edit. Other Livewire requests may send pending state earlier.

#### Applying accessibility and input state

Apply the supplied input props as shown in the examples. Use `id` on the primary input to associate Filament's label, and label additional inputs separately. `ariaDescribedBy` associates helper text, validation messages, and the required description; append additional description IDs instead of replacing it. Custom wrappers must [preserve these associations](overview#preserving-accessibility-in-custom-field-views).

### Passing configuration from PHP

You can pass configuration to your renderer using `rendererConfiguration()`:

```php
use Filament\Forms\Components\JsField;
use Illuminate\Support\Facades\Vite;

JsField::make('location')
    ->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
    ->rendererConfiguration(['zoom' => 12])
```

Read it from `props.configuration.zoom` in your component. Configuration is separate from built-in props such as `value` and `onChange`, so it cannot override the state binding. Only pass JSON-serializable data intended for the browser, not secrets. Modifying configuration in JavaScript does not write back to PHP.

In the Vue example, add `configuration` to `defineProps()`. In the Svelte example, read `configuration` from `$props()` alongside `value` and the other props.

<UtilityInjection set="formFields" version="5.x">As well as allowing a static value, the `rendererConfiguration()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>

#### Updating configuration from another field

For example, you can use the selected country to configure a location picker:

```php
use Filament\Forms\Components\JsField;
use Filament\Schemas\Components\Utilities\Get;
use Illuminate\Support\Facades\Vite;

JsField::make('location')
    ->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
    ->rendererConfiguration(static fn (Get $get): array => [
        'country' => $get('country'),
        'zoom' => 12,
    ])
```

Filament reevaluates the closure when the field renders on the server, then updates the renderer without remounting it. Use [`live()`](overview#the-basics-of-reactivity) on the country field to send edits immediately. Requests that skip rendering this field do not update its PHP-derived configuration.

Treat configuration as immutable. Pass `null` to `rendererConfiguration()` to clear it. For a [reusable field](#building-reusable-plugin-fields), override `getRendererConfiguration(): array` instead; its default is `[]`.

### Accessing schema utilities

Accept `utilities` alongside `host` and `props` in your mount function to access schema utilities. For example, an interaction handler can read or change a sibling country field:

```js
const country = utilities.$get('country')
await utilities.$set('country', 'GB')
```

`$get(path, isAbsolute)` and `$set(path, value, isAbsolute, isLive = false)` use paths relative to the field's container unless `isAbsolute` is `true`. `$set()` is deferred unless `isLive` is `true`. It does not apply the target field's read-only configuration or state binding modifiers, so use `props.onChange()` for ordinary edits to this field.

`utilities.$statePath` is this field's absolute state path, and `utilities.$state` reads its current value. Treat returned state as read-only. Keep `utilities` intact when passing it to your framework: spreading or destructuring its getters captures their current values. Reads do not subscribe to sibling-field changes; use [PHP configuration](#passing-configuration-from-php) for updates after a server render.

In Svelte components, local names beginning with `$` are reserved. Keep property access such as `utilities.$get()`, or alias destructured functions: `const { $get: getState, $set: setState } = utilities`.

#### Calling PHP methods

You can [expose methods on your custom field class](#calling-field-methods-from-javascript) and call them through `utilities`, such as `utilities.$geocodeAddress({ address })`. The same method exposure and authorization rules apply to Blade and JavaScript-rendered fields. See [calling an exposed method from a framework](#calling-an-exposed-method-from-a-framework) for an example.

For methods on the owning Livewire component rather than the field class, use `utilities.$wire`. For example, if your Livewire component defines `saveLocation()`:

```js
await utilities.$wire.$call('saveLocation')
```

`$wire` is the original Livewire proxy. Direct writes bypass the field's state binding timing; prefer `props.onChange()` for field edits. Handle request failures, and stop using utilities when the renderer is destroyed.

### Typing renderers

Use [`--typescript` or `--ts`](#generating-a-javascript-field) to generate typed starters. Filament includes the declarations in its Composer package, without a JavaScript runtime dependency. See [type-checking ES modules](../advanced/assets#type-checking-es-modules) for compiler configuration and framework-specific checks.

For an existing renderer, add a type alias to your application's `tsconfig.json`, relative to that configuration file (or its `baseUrl`, if configured):

```json
{
    "compilerOptions": {
        "paths": {
            "@filament/forms/js-field": ["./vendor/filament/forms/resources/js/types/js-field.d.ts"]
        }
    }
}
```

Use `JsFieldRenderer` to type your mount function's state and PHP configuration. The following shows where to apply the type to one of the implementations above:

```ts
import type { JsFieldRenderer } from '@filament/forms/js-field'

type Location = { latitude: number | null; longitude: number | null } | null
type Configuration = { zoom: number }

const mountLocationPicker: JsFieldRenderer<Location, Configuration> = ({ host, props, utilities }) => {
    // Mount your component with `props` and `utilities`.

    return {
        update(nextProps) {
            // Update it with `nextProps.value` and `nextProps.configuration`.
        },
        destroy() {
            // Unmount it and remove subscriptions.
        },
    }
}

export default mountLocationPicker
```

You can also import `JsFieldRendererContext`, `JsFieldProps`, `JsFieldUtilities`, and `JsFieldRendererInstance` to type individual parts of your implementation. `JsFieldProps` includes callbacks and describes both mount and update props. `JsFieldInitialProps` remains available as a deprecated compatibility alias for `JsFieldProps`.

State and configuration are deeply read-only snapshots in TypeScript, not runtime-frozen objects. Unspecified types must be narrowed before use. Keep `import type` so your bundler does not try to load the declaration file, and keep PHP validation even when using TypeScript.

#### Typing exposed methods

For [exposed PHP methods](#calling-an-exposed-method-from-a-framework), supply a third type argument, such as `JsFieldRenderer<Location, Configuration, Methods>`. Declare the methods your field exposes; PHP signatures are not automatically translated into TypeScript:

```ts
interface Methods {
    $geocodeAddress(argumentsObject: { address: string }): Promise<Location>
}
```

`JsFieldLivewire` declares `$call()`, `$get()`, and `$set()` on `$wire`. Use `$call()` for custom Livewire methods. Other Livewire APIs remain available at runtime; extend the type locally to match your installed Livewire version when using them. In a plugin repository, point the type alias at your development Composer installation.

### Synchronizing and disposing renderers

Return `update(props)` and `destroy()` from your mount function, or return a promise for that object. Server changes update the renderer and cancel a pending debounce when the incoming value differs from the pending edit. Removal cancels pending commits and calls `destroy()`. A renderer that finishes initializing after removal is immediately destroyed.

Clean up subscriptions, event listeners, and framework roots in `destroy()`, including when the host is already detached. Stop DOM work synchronously even if cleanup returns a promise. Changes to the renderer URL or state binding configuration remount the renderer with current state; changes to `rendererConfiguration()` and input settings such as `isInvalid`, `isRequired`, `isDisabled`, and `isReadOnly` update it in place. Keep persistent data in the field value, not only in component-local state.

#### Understanding the boundary with Blade and Alpine

Render only inside `host`, whose contents are ignored by Livewire. Do not change its parent or siblings. PHP records, operations, closures, Blade slots, and rendered Filament child schemas are not automatically available inside it. Compute serializable data in `rendererConfiguration()` and keep server-rendered actions, hints, and child schemas outside the host. Filament and Livewire still own field state, validation, authorization, and HTTP responses.

Alpine directives and magic properties are not injected into React, Vue, or Svelte templates. Use the framework's event, reference, and lifecycle APIs, native DOM events, or the supplied `$wire` for Livewire-specific operations. There are no dedicated framework adapters for uploads, actions/modals, or sibling-state subscriptions; access to `$wire` does not automatically make those APIs reactive in your framework. Clean up listeners and subscriptions using their owning API's cleanup mechanism.

If you already load the renderer through another script, you may pass a trusted `Filament\Support\RawJs` function expression to `renderer()` instead of a URL.

#### Handling renderer errors

If importing, mounting, or `update()` fails, Filament shows an accessible error without changing the field value. Check the browser console, correct the module, then reload. Diagnostics include renderer URLs and original errors, so avoid including sensitive data in either. Cleanup failures are reported without blocking other fields' cleanup.

Clean up resources yourself if mounting fails before returning lifecycle methods. Handle errors in framework rendering, event handlers, and independent asynchronous work with your framework's error handling. Manage ordering and cancellation for asynchronous updates: Filament does not serialize them or await cleanup before remounting.

### Customizing the Blade view

A field using `HasJsRenderer` does not need a Blade view, but you can set its `$view` property or call `view()` to render a custom view instead:

```php
use App\Filament\Forms\Components\LocationPicker;

LocationPicker::make('location')
    ->view('filament.forms.components.location-picker')
```

The custom view replaces the default output; it does not automatically mount the JavaScript renderer. To keep the renderer inside your own Blade layout, render the field's `toEmbeddedHtml()` output in `resources/views/filament/forms/components/location-picker.blade.php`:

```blade
<div>
    {!! $field->toEmbeddedHtml() !!}
</div>
```

This output already includes the field wrapper, so do not add another field wrapper around it. Keep any additional Blade content outside the renderer's `host`.

### Building reusable plugin fields

Use `HasJsRenderer` on your own `Field` class to build a reusable field without a Blade view. Implement `getRenderer()` to return your module URL:

```php
namespace Vendor\LocationPicker\Forms\Components;

use Filament\Forms\Components\Concerns\HasJsRenderer;
use Filament\Forms\Components\Field;
use Filament\Support\Facades\FilamentAsset;

class LocationPicker extends Field
{
    use HasJsRenderer;

    public function getRenderer(): string
    {
        return FilamentAsset::getScriptSrc('location-picker', 'vendor/location-picker');
    }
}
```

Follow the [plugin guide](../plugins/getting-started#creating-a-plugin) to set up your package and [publish its prebuilt module](../advanced/assets#publishing-es-modules-in-plugins) under this asset ID and package name. Consumers can then use `LocationPicker::make('location')->live()` without a renderer URL, a Blade view, or changes to their Vite setup.

Use the same mount function as for `JsField`. To pass [configuration](#passing-configuration-from-php), override `getRendererConfiguration(): array` and return your field's evaluated configuration values. You can expose those values through the usual [fluent configuration methods](#adding-a-configuration-method-to-a-custom-field-class).

## Calling field methods from JavaScript

You can call methods on your custom field class from a Blade view or a [JavaScript renderer](#rendering-fields-with-javascript-frameworks). For example, a location picker might call a geocoding service in PHP. Expose the method using `#[Exposed]`.

### Exposing a method

Add `#[Exposed]` to a public instance method on your field. This Blade-based example also applies to a class using [`HasJsRenderer`](#building-reusable-plugin-fields):

```php
use Filament\Forms\Components\Field;
use Filament\Support\Components\Attributes\Exposed;

class LocationPicker extends Field
{
    protected string $view = 'filament.forms.components.location-picker';

    #[Exposed]
    public function geocodeAddress(string $address): array
    {
        // Perform geocoding logic...

        return [
            'latitude' => $latitude,
            'longitude' => $longitude,
        ];
    }
}
```

<Aside variant="warning">
    Only methods marked with `#[Exposed]` can be called from JavaScript. The attribute does not authorize access to the method's data or operations. Validate client-supplied arguments and [authorize sensitive operations](../advanced/security#authorization) inside the method.

    Exposed methods remain callable when the field is [disabled](overview#disabling-a-field), since read methods may still be needed to display its value. If a method changes the field's value and should respect its disabled state, check `$this->isDisabled()` before making changes.
</Aside>

Arguments must be a JSON-serializable object keyed by PHP parameter name. Filament binds each call to the current field, including nested repeater instances. The returned promise resolves to the method's return value, or `null` if the component or exposed method is missing. Calls make Livewire requests and can send pending deferred edits, regardless of `live()`.

Use the PHP method's casing in JavaScript. Built-in utility and Alpine magic names are reserved; a method named `get`, for example, is available through `$callSchemaComponentMethod()` without replacing `$get()`. Do not register Alpine magics with the same names as your exposed methods.

### Calling the method from JavaScript

In your Blade view, call the exposed method using its `$`-prefixed name. Pass an object keyed by PHP parameter name. The utility is bound to this field, so you do not need its key. You can also use `$callSchemaComponentMethod('geocodeAddress', { address: this.address })`, including when a method name collides with a built-in utility such as `$get()`:

```blade
<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    <div
        x-data="{
            address: '',
            coordinates: null,
            async geocode() {
                this.coordinates = await this.$geocodeAddress(
                    { address: this.address },
                )
            },
        }"
    >
        <input type="text" x-model="address" />
        <button type="button" x-on:click="geocode">Geocode</button>

        <template x-if="coordinates">
            <p x-text="`${coordinates.latitude}, ${coordinates.longitude}`"></p>
        </template>
    </div>
</x-dynamic-component>
```

### Calling an exposed method from a framework

In a JavaScript renderer, the exposed methods are available on `utilities`. Call them from an interaction handler and pass the result to `props.onChange()`:

```js
const coordinates = await utilities.$geocodeAddress({ address: '10 Downing Street, London' })

if (coordinates !== null) {
    props.onChange(coordinates)
}
```

You can also call `utilities.$callSchemaComponentMethod('geocodeAddress', { address })`. Handle request failures in your component, and avoid updating it if it has been destroyed while awaiting the response. See [typing renderers](#typing-renderers) to declare named methods in TypeScript.

### Preventing re-renders

By default, calling an exposed method triggers a re-render of the Livewire component. If your method only returns data and does not change state or PHP props that need rendering, add Livewire's `#[Renderless]` attribute. Other pending updates or calls in the same request may still require rendering:

```php
use Filament\Forms\Components\Field;
use Filament\Support\Components\Attributes\Exposed;
use Livewire\Attributes\Renderless;

class LocationPicker extends Field
{
    protected string $view = 'filament.forms.components.location-picker';

    #[Exposed]
    #[Renderless]
    public function geocodeAddress(string $address): array
    {
        // ...
    }
}
```
