Filament: Instant Field Visibility with visibleJs() - No Livewire Roundtrip

2026-03-15 Filament v4

You need to show or hide a field based on another field's value. The go-to approach: slap ->live() on the controlling field and use ->visible() with a closure. It works, but every change fires a Livewire request and re-renders the form. On a multi-step form with several conditional fields, that lag adds up.

Filament v4 introduced visibleJs() and hiddenJs(): they evaluate JavaScript expressions in the browser. No network request. No re-render. Instant.


The Old Way: ->live() + ->visible()

use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Get;
 
Select::make('role')
->options([
'user' => 'User',
'staff' => 'Staff',
])
->live()
 
Toggle::make('is_admin')
->visible(fn (Get $get): bool => $get('role') === 'staff')

Every change sends state to the server, PHP evaluates the closure, the component re-renders. For one field, tolerable. For a wizard with five dependent fields — users feel it.


The Better Way: visibleJs()

Drop ->live(). Replace ->visible() with visibleJs():

use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
 
Select::make('role')
->options([
'user' => 'User',
'staff' => 'Staff',
])
// no ->live() needed
 
Toggle::make('is_admin')
->visibleJs(<<<'JS'
$get('role') === 'staff'
JS)

Filament provides a $get() JavaScript utility that mirrors the PHP Get instance. It reads the current value of any sibling field from Alpine.js state. Purely client-side.

hiddenJs() does the inverse:

Toggle::make('is_admin')
->hiddenJs(<<<'JS'
$get('role') !== 'staff'
JS)

If you use both on the same component, the field only shows when both conditions agree.


Practical Example: Multi-Step Event Form

Here's where this shines. A Wizard for creating events — each step has conditional fields. With ->live(), every toggle change re-renders the entire step.

app/Filament/Resources/EventResource/Pages/CreateEvent.php

use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
 
public function form(Form $form): Form
{
return $form
->schema([
Wizard::make([
Step::make('Basics')
->schema([
TextInput::make('name')
->required(),
 
Radio::make('type')
->options([
'in_person' => 'In Person',
'virtual' => 'Virtual',
'hybrid' => 'Hybrid',
])
->required(),
 
TextInput::make('venue')
->visibleJs(<<<'JS'
['in_person', 'hybrid'].includes($get('type'))
JS),
 
TextInput::make('stream_url')
->label('Stream URL')
->url()
->visibleJs(<<<'JS'
['virtual', 'hybrid'].includes($get('type'))
JS),
]),
 
Step::make('Registration')
->schema([
Toggle::make('requires_registration'),
 
Section::make('Registration Settings')
->schema([
TextInput::make('max_attendees')
->numeric(),
 
DateTimePicker::make('registration_deadline'),
 
Toggle::make('has_waitlist')
->label('Enable waitlist'),
 
TextInput::make('waitlist_limit')
->numeric()
->visibleJs(<<<'JS'
$get('has_waitlist') == true
JS),
])
->visibleJs(<<<'JS'
$get('requires_registration') == true
JS),
]),
 
Step::make('Pricing')
->schema([
Select::make('pricing_model')
->options([
'free' => 'Free',
'paid' => 'Paid',
'donation' => 'Pay What You Want',
]),
 
TextInput::make('price')
->numeric()
->prefix('$')
->visibleJs(<<<'JS'
$get('pricing_model') === 'paid'
JS),
 
TextInput::make('minimum_donation')
->numeric()
->prefix('$')
->visibleJs(<<<'JS'
$get('pricing_model') === 'donation'
JS),
]),
]),
]);
}

Pick "Hybrid" — both venue and stream URL appear without a flicker. Toggle requires_registration — the entire section slides in. Zero roundtrips.


Things to Know

  1. $get() works like its PHP counterpart — same state paths, relative paths work within nested schemas.

  2. Toggles aren't strict booleans — use == true rather than === true, since Alpine.js may store 1 or "1".

  3. Pairs well with afterStateUpdatedJs() — need to set values client-side too (like auto-generating a slug)? Use $set() in afterStateUpdatedJs(). You can build fully responsive forms with zero ->live() fields.

  4. Server-side validation still runs on submitvisibleJs() only controls what the user sees. If you need hidden fields excluded from save, handle it in mutateFormDataBeforeCreate() or add a matching ->visible() for the server side.

  5. Works on Sections and Fieldsets too — not just individual fields. The registration Section above is a good example.

For forms where conditional visibility is purely a UX concern, visibleJs() beats ->live() every time.

A few of our Premium Examples: