Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Searchable Select Laravel Package

williamug/searchable-select

A feature-rich searchable dropdown component for Laravel Livewire 3 & 4, built with Alpine.js and Tailwind CSS. Supports real-time search, multi-select with tags, grouped options, dark mode, full keyboard accessibility, and cascading dependent dropdowns.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a single Blade component (<x-searchable-select>) that replaces a standard <select> with a searchable, accessible dropdown. It is a UI-only component — it does not query your database, so your Livewire component owns the data and the package owns the rendering.

1. Install

composer require williamug/searchable-select

The service provider auto-registers. You do not need to publish anything to get started.

2. Wire the package views into Tailwind (do this before writing any markup)

This is the step that trips people up and it is the first thing to verify. The component's classes live in vendor/williamug/searchable-select/resources/views, which Tailwind does not scan by default. If you skip this, the dropdown renders unstyled or appears invisible (the panel may be hidden/absolute with no positioning).

Tailwind v4 — in resources/css/app.css:

@import 'tailwindcss';
@source '../../vendor/williamug/searchable-select/resources/views/**/*.blade.php';

Tailwind v3 — in tailwind.config.js:

content: [
  './resources/**/*.blade.php',
  './vendor/williamug/searchable-select/resources/views/**/*.blade.php',
],

Then npm run build (or keep npm run dev running).

3. First use case: a searchable foreign-key picker

This is the 80% case — you have a country_id column and 200+ countries, and a plain <select> is unusable.

// app/Livewire/ContactForm.php
class ContactForm extends Component
{
    public $countries = [];   // options source
    public $country_id;       // bound value

    public function mount(): void
    {
        $this->countries = Country::orderBy('name')->get(['id', 'name']);
    }

    public function save(): void
    {
        $this->validate(['country_id' => 'required|exists:countries,id']);
    }
}
<x-searchable-select
    wire:model="country_id"
    :options="$countries"
    placeholder="Select a country"
    search-placeholder="Search countries..."
/>

@error('country_id') <span class="text-red-500">{{ $message }}</span> @enderror

Where to look first: the props table in the README is the whole API surface — 12 props, no config file, no facade. Read that table once and you know the package. Set option-value / option-label consciously; the defaults (id, name) will silently render blank rows if your columns differ.

Implementation Patterns

Pattern 1: Constrain the query, don't ship the table

options is passed as a collection into the component, which serialises it into the Livewire payload. A 20,000-row table will bloat every subsequent request. Select only the columns you need and filter server-side where possible:

$this->countries = Country::query()
    ->orderBy('name')
    ->limit(500)
    ->get(['id', 'name']);   // never ->all() with all columns

Pattern 2: Custom keys for non-id/name schemas

option-value and option-label are string keys resolved on each option, so they work identically for Eloquent models, plain arrays, and stdClass. Pick stable, human-readable label columns — name, not id, and definitely not a JSON blob.

<x-searchable-select
    wire:model="country_code"
    :options="$countries"
    option-value="code"
    option-label="country_name"
/>

Pattern 3: Multi-select with array-valued properties

multiple requires the bound property to be an array. Always initialise it (public $selected_skills = [];) — null is a common source of "nothing happens" bugs on first selection.

<x-searchable-select
    wire:model="selected_skills"
    :options="$skills"
    :multiple="true"
/>

Validation for arrays is the standard Laravel form:

$this->validate([
    'selected_skills'   => 'array|min:1|max:5',
    'selected_skills.*' => 'exists:skills,id',
]);

Pattern 4: Cascading dropdowns via updated* hooks

The component uses $wire.entangle() internally, so a selection triggers a normal Livewire update and your updated{PropertyName}() hook fires. There is no custom event to listen for — this is the idiomatic integration:

public function updatedCountryId($value): void
{
    $this->regions = $value
        ? Region::where('country_id', $value)->orderBy('name')->get()
        : [];

    // Reset children — the component will render nothing selected,
    // but your PHP state must be cleared too or stale IDs persist on save.
    $this->region_id = null;
    $this->city_id = null;
    $this->cities = [];
}
<x-searchable-select
    wire:model="region_id"
    :options="$regions"
    :placeholder="empty($regions) ? 'Select a country first' : 'Select region'"
    :disabled="!$country_id"
/>

Note the hook name: updatedCountryId, not updatedCountry_id (Livewire converts country_id → CountryId). Getting this wrong is silent — no error, no reload.

Pattern 5: Grouped options for long, categorised lists

Only use grouped when you can genuinely organise the data; the shape is [{label, options: [...]}] and both keys are configurable:

public $locations = [
    ['label' => 'North America', 'options' => [['id' => 1, 'name' => 'United States'], ...]],
    ['label' => 'Europe',        'options' => [['id' => 4, 'name' => 'United Kingdom'], ...]],
];
<x-searchable-select
    wire:model="country_id"
    :options="$locations"
    :grouped="true"
    group-label="label"
    group-options="options"
/>

The same option-value / option-label props still apply to the inner options.

Pattern 6: Form object / model binding

Because it is a plain Blade component, it composes with wire:model on Livewire\Form objects and model attributes exactly like any input:

<x-searchable-select wire:model="form.country_id" :options="$countries" />

For a filter bar, bind to a query-string property (#[Url] public $category_id;) and let the entangle do the rest.

Gotchas and Tips

Tailwind purging is the #1 failure mode. If the dropdown looks unstyled, is invisible, or the panel renders in the wrong place, 95% of the time the @source / content entry is missing or the assets weren't rebuilt after composer require. Check the built CSS actually contains something like .absolute from the package path. In CI, remember npm run build must run after composer install.

Do not pass selected-value. The README is explicit: wire:model is the only binding mechanism, backed by entangle. Adding a manual :selected-value will conflict with it. Likewise there is no wire:model.live vs .blur distinction to manage — the entangle pushes on every pick.

Multi-select properties must be arrays, and PHP state must be reset deliberately. The component won't clear region_id for you when a parent changes. Every cascading reset is your responsibility, in both directions (dropdown UI and PHP property). Test the "select parent, pick child, change parent, save" path — that's where stale IDs leak into the database.

disabled is visual + interactive, not validation. It prevents clicking, but a crafted request can still submit the property. Always validate with exists:/in: rules server-side — the component is not a security boundary.

Watch the Livewire payload. :options is serialised each render. If a page feels sluggish, check the Livewire payload size in the network tab; the fix is almost always a select(...) with fewer columns or a lower limit.

Customisation goes through publishing, and then it's yours.

php artisan vendor:publish --tag=searchable-select-views

After publishing, package updates will not reach your overridden views. Prefer wrapping the component in your own Blade component (<x-ui.select>) that forwards props, rather than editing the vendor view — you keep upgradeability and get one place to change styling app-wide.

Compatibility is narrow-by-design. PHP 8.2+, Laravel 11/12/13, Livewire 3 or 4, Tailwind 3 or 4. Nothing surprising, but verify your Livewire major before debugging anything else — Alpine is bundled with Livewire, so an Alpine version mismatch from a separate Alpine install is a realistic culprit for "panel won't open".

Maturity caveat: pin it and test the upgrade. This is a 2-star, low-download package (dependents: 0) with a single maintainer. That is not disqualifying — it's a small, dependency-free, MIT component you could vendor in an afternoon — but take the standard precautions: commit the exact version in composer.lock, run your test suite after bumps, and be prepared to fork. If the dropdown is load-bearing for a critical flow, publish the views so a breaking change or abandonment doesn't block you.

Debugging checklist, in order: (1) is the built CSS current and does it contain the package classes? (2) is wire:model pointing at a real public property with the right type (array for multiple)? (3) do option-value/option-label keys actually exist on the data? (4) is the updatedPropertyName hook cased correctly? (5) is another Alpine component on the page swallowing the click (nesting inside a modal or an existing x-data scope)? Nine times out of ten it's step 1 or 3.

Testing: the component is client-side Alpine, so browser tests (Laravel Dusk / Pest browser plugin) are required to exercise open/search/select. For the rest, unit-test your Livewire component's updated* hooks and validation rules directly with Livewire::test() — that covers the logic that actually breaks, and skips the styling concerns entirely.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc