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.
->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.
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.
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.
$get() works like its PHP counterpart — same state paths, relative paths work within nested schemas.
Toggles aren't strict booleans — use == true rather than === true, since Alpine.js may store 1 or "1".
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.
Server-side validation still runs on submit — visibleJs() 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.
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: