asantibanez/livewire-select
Livewire Select is a Laravel Livewire component for building dependent, searchable select dropdowns. Chain selects to Eloquent models, dynamically filter options, and handle large datasets—ideal for country/city, category/subcategory, and other cascading form fields.
Install with Composer and nothing else — the package is a pure Livewire component, no config file or asset pipeline to publish for basic use:
composer require asantibanez/livewire-select
As of v2.3.0 the package targets Livewire 3 and Laravel 11 / 12. That means the version constraint is your upgrade lever: on Livewire 2 the latest tag will not install, so pin to the last 1.x release (composer require "asantibanez/livewire-select:^1.0") and stay there until you move the app to Livewire 3. On Livewire 3 + Laravel 11/12 you can take the current tag without ceremony.
Render the component from inside any existing Livewire component's Blade view. The two required pieces are a name (used to identify the selection in events) and an options array in the shape the package expects — a list of arrays with value and description keys:
<livewire:livewire-select
name="country"
:options="$countries"
placeholder="Select a country"
/>
public function mount()
{
$this->countries = Country::all()
->map(fn ($c) => ['value' => $c->id, 'description' => $c->name])
->toArray();
}
The component does not bind to wire:model. It communicates by emitting an inputSelected event with a payload of name and value. That is the whole mental model: render a select, listen for what it picked. The first use case to build on day one is a dependent pair — country → state — because it exercises the full loop (render, listen, update options, re-render) in about fifteen lines:
protected $listeners = ['inputSelected'];
public function inputSelected($payload)
{
if ($payload['name'] === 'country') {
$this->states = State::where('country_id', $payload['value'])
->get()
->map(fn ($s) => ['value' => $s->id, 'description' => $s->name])
->toArray();
}
}
Cascading selects. Keep all selection state in the parent Livewire component. The child select components are dumb renderers of whatever options you pass down. When the parent's inputSelected handler changes $states, Livewire re-renders the second <livewire:livewire-select> with new options automatically. For three-level chains (country → state → city), branch on $payload['name'] in a single handler rather than registering multiple listeners.
Resetting downstream selects. When the parent changes, the second select still displays the old label internally. Guard against stale values by passing :value="null" (or your reset sentinel) explicitly when you rebuild the options, and by clearing the corresponding parent property in the same handler. Treat value as a controlled prop you re-assert on every cascade.
Form integration. Because there is no HTML name attribute and no wire:model, the value never reaches $request on its own. Mirror it into a public property in the parent ($this->countryId = $payload['value']) and validate against that property. For edit forms, hydrate the initial value through the value prop and re-hydrate old() on validation failure — otherwise the select shows a placeholder while the rest of the form repopulates.
Searchable for large option sets. Add searchable when the list exceeds a couple dozen entries. Since filtering happens inside a nested Livewire component, keep the option list bounded (top 50 rows, indexed columns, no SELECT * on a million-row table) — a searchable select backed by a full table scan will feel worse than a plain one.
Reuse via your own wrapper. If you render the same select shape repeatedly, wrap it in a project Blade component (<x-country-select :selected="$countryId" />) so you centralise the value/description mapping and the name string. This is also the cleanest place to enforce a consistent option-mapping helper (->map(fn ($m) => ['value' => $m->id, 'description' => $m->label])).
Testing. Assert against the event contract, not the markup: Livewire::test(MyForm::class)->dispatch('inputSelected', ['name' => 'country', 'value' => 1])->assertSet('states', [...]). For the child itself, assertDispatched('inputSelected') on a direct test of the select component. On Livewire 3 these helpers (dispatch, assertDispatched) are the canonical API — if you have Livewire 2-era tests using emit/assertEmitted, update them at the same time you take v2.3.0.
Styling. Publish the views (php artisan vendor:publish --tag=livewire-select:views) and edit the Blade directly rather than fighting it with CSS overrides from outside — the markup is shallow enough that a few Tailwind class swaps are the fastest path to matching your design system.
composer require resolves to a 1.x tag on your Livewire 2 app, that is Composer doing the right thing, not a broken package — pin deliberately and don't force --with-dependencies to drag the current tag down onto Livewire 2.name. Every select on the page emits the same inputSelected event. If you don't branch on $payload['name'], a city change will happily overwrite your country. This is the single most common bug with this package.->map(...)->toArray() at the boundary.description is the label key, value is the identifier. Mixing these up (or using label/id from a copied snippet) produces an empty dropdown with no error.wire:model, no name attribute. Don't try to submit the select with a plain form post, and don't expect $this->validate() to see it unless you mirrored the value into a property. wire:model exists in Livewire 3 but this component still doesn't expose it — don't assume the upgrade changed the contract.wire:key inside loops. Rendering selects in a @foreach without a stable key causes Livewire to reuse component state across rows and show the wrong selection.value in the parent handler.inputSelected payload) is unchanged, so your parent handlers keep working. What does change is Livewire's event API: emit/emitUp/assertEmitted become dispatch/dispatch()->to()/assertDispatched. Any project code or tests touching those need updating in the same deploy, and it's worth re-testing cascades after the upgrade since component lifecycle timing shifted.@dump($options)) — nine times out of ten it's a Collection, a wrong key, or a null option list from a query that returned nothing.How can I help you explore Laravel packages today?