A beautiful, searchable dropdown component for Laravel Livewire 3 & 4 applications. Built with Alpine.js and Tailwind CSS - no external dependencies required!
Explore all features with live, interactive examples:
- Basic Select - Simple searchable dropdown
- Multi-Select - Select multiple options with tags
- Grouped Options - Organize by categories
- API Integration - Dynamic loading from endpoints
- Advanced Examples - All features combined
👉 Run the demo locally with Docker or PHP
- Real-time search - Client-side filtering as you type
- Multi-select support - Select multiple options at once
- Ajax/API integration - Fetch options dynamically from endpoints
- Grouped options - Organize options into categories
- Clear button - Easily clear selections
- Dark mode support - Automatically adapts to your theme
- Accessible - Keyboard navigation and ARIA attributes
- Livewire 3 & 4 compatible - Works with both versions
- Responsive - Mobile-friendly design
- Disabled state - Conditional disabling support
- Flexible data - Works with models, arrays, collections
- Dependent dropdowns - Perfect for cascading selects
- Customizable - Override styles with Tailwind classes
- Zero config - Works out of the box
- PHP 8.1+
- Laravel 9.x, 10.x, 11.x, or 12.x
- Livewire 3.x or 4.x
- Tailwind CSS 3.x+
- Alpine.js (bundled with Livewire)
Install via Composer:
composer require williamug/searchable-selectRun the installation command:
php artisan install:searchable-selectThat's it! The component will be copied to resources/views/components/searchable-select.blade.php
If you want to overwrite an existing installation:
php artisan install:searchable-select --forceLivewire Component:
<?php
namespace App\Livewire;
use App\Models\Country;
use Livewire\Component;
class ContactForm extends Component
{
public $countries;
public $country_id;
public function mount()
{
$this->countries = Country::orderBy('name')->get();
}
public function render()
{
return view('livewire.contact-form');
}
}Blade View:
<x-searchable-select
:options="$countries"
wire-model="country_id"
:selected-value="$country_id"
placeholder="Select Country"
search-placeholder="Search countries..."
/>If you have related dropdowns (e.g. Country → Region → City), you can easily update the options based on the selected value of the parent dropdown.
Livewire Component:
<?php
namespace App\Livewire;
use App\Models\{Country, Region, City};
use Livewire\Component;
class LocationSelector extends Component
{
public $countries, $regions = [], $cities = [];
public $country_id, $region_id, $city_id;
public function mount()
{
$this->countries = Country::orderBy('name')->get();
}
public function updatedCountryId()
{
$this->regions = Region::where('country_id', $this->country_id)
->orderBy('name')->get();
$this->region_id = null;
$this->city_id = null;
$this->cities = [];
}
public function updatedRegionId()
{
$this->cities = City::where('region_id', $this->region_id)
->orderBy('name')->get();
$this->city_id = null;
}
public function render()
{
return view('livewire.location-selector');
}
}Blade View:
<div class="grid grid-cols-3 gap-4">
<!-- Country -->
<div>
<label>Country</label>
<x-searchable-select
:options="$countries"
wire-model="country_id"
:selected-value="$country_id"
placeholder="Select Country"
/>
</div>
<!-- Region -->
<div>
<label>Region</label>
<x-searchable-select
:options="$regions"
wire-model="region_id"
:selected-value="$region_id"
:placeholder="empty($regions) ? 'First select a country' : 'Select Region'"
:disabled="!$country_id"
/>
</div>
<!-- City -->
<div>
<label>City</label>
<x-searchable-select
:options="$cities"
wire-model="city_id"
:selected-value="$city_id"
:placeholder="empty($cities) ? 'First select a region' : 'Select City'"
:disabled="!$region_id"
/>
</div>
</div>| Prop | Type | Default | Description |
|---|---|---|---|
options |
Array/Collection | [] |
List of options to display |
wireModel |
String | '' |
Livewire property to bind (required) |
selectedValue |
Mixed | null |
Currently selected value |
placeholder |
String | 'Select option' |
Placeholder when nothing selected |
searchPlaceholder |
String | 'Search...' |
Search input placeholder |
disabled |
Boolean | false |
Disable the dropdown |
emptyMessage |
String | 'No options available' |
Message when options is empty |
optionValue |
String | 'id' |
Key for option values |
optionLabel |
String | 'name' |
Key for option labels |
multiple |
Boolean | false |
Enable multi-select mode |
clearable |
Boolean | true |
Show clear button when value selected |
apiUrl |
String | null |
API endpoint for dynamic options |
apiSearchParam |
String | 'search' |
Query parameter name for API search |
grouped |
Boolean | false |
Enable grouped options |
groupLabel |
String | 'label' |
Key for group labels |
groupOptions |
String | 'options' |
Key for group options array |
Select multiple options at once:
public $selected_countries = []; // Array for multiple selections<x-searchable-select
:options="$countries"
wire-model="selected_countries"
:selected-value="$selected_countries"
:multiple="true"
placeholder="Select Countries"
/>The clear button is enabled by default. Disable it if needed:
<x-searchable-select
:options="$countries"
wire-model="country_id"
:selected-value="$country_id"
:clearable="false"
/>Organize options into groups:
public $locations = [
[
'label' => 'North America',
'options' => [
['id' => 1, 'name' => 'United States'],
['id' => 2, 'name' => 'Canada'],
['id' => 3, 'name' => 'Mexico'],
]
],
[
'label' => 'Europe',
'options' => [
['id' => 4, 'name' => 'United Kingdom'],
['id' => 5, 'name' => 'France'],
['id' => 6, 'name' => 'Germany'],
]
],
];<x-searchable-select
:options="$locations"
wire-model="country_id"
:selected-value="$country_id"
:grouped="true"
placeholder="Select Country"
/>Fetch options dynamically from an API endpoint:
<x-searchable-select
:options="[]"
wire-model="user_id"
:selected-value="$user_id"
api-url="{{ route('api.users.search') }}"
api-search-param="q"
placeholder="Search users..."
/>Your API endpoint should return JSON:
// routes/api.php
Route::get('/users/search', function (Request $request) {
$users = User::where('name', 'like', '%' . $request->q . '%')
->limit(20)
->get(['id', 'name']);
return response()->json(['data' => $users]);
});Combine multiple selection with API search:
<x-searchable-select
:options="[]"
wire-model="selected_users"
:selected-value="$selected_users"
:multiple="true"
api-url="{{ route('api.users.search') }}"
placeholder="Select Team Members"
/>public $statuses = [
['id' => 'draft', 'name' => 'Draft'],
['id' => 'published', 'name' => 'Published'],
['id' => 'archived', 'name' => 'Archived'],
];<x-searchable-select
:options="$statuses"
wire-model="status"
:selected-value="$status"
/>public $products; // Has 'sku' and 'product_name' fields<x-searchable-select
:options="$products"
wire-model="product_sku"
:selected-value="$product_sku"
option-value="sku"
option-label="product_name"
/><x-searchable-select
:options="$countries"
wire-model="country_id"
:selected-value="$country_id"
class="border-2 border-blue-500 rounded-xl"
/>protected $rules = [
'country_id' => 'required|exists:countries,id',
'city_id' => 'required|exists:cities,id',
];<x-searchable-select
:options="$countries"
wire-model="country_id"
:selected-value="$country_id"
/>
@error('country_id')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderrorMake sure your tailwind.config.js includes the component path:
export default {
content: [
'./resources/views/**/*.blade.php',
'./resources/views/components/**/*.blade.php',
],
}Create a dedicated component for common use cases:
resources/views/components/country-select.blade.php:
<x-searchable-select
:options="\App\Models\Country::orderBy('name')->get()"
wire-model="{{ $wireModel }}"
:selected-value="$selectedValue"
placeholder="Select Country"
search-placeholder="Search countries..."
{{ $attributes }}
/>Usage:
<x-country-select wire-model="country_id" :selected-value="$country_id" />- Verify Alpine.js is loaded (part of Livewire 3+)
- Check browser console for JavaScript errors
- Ensure no JavaScript conflicts
- Verify
selectedValuematches an option value - Check
optionValueprop matches your data structure - Ensure the value exists in options array
- Run
npm run buildto compile Tailwind - Verify component path in
tailwind.config.js - Check for CSS conflicts
- < 1,000 options: Client-side filtering (default) works great
- > 1,000 options: Consider server-side search with
wire:model.live.debounce - Very large datasets: Implement pagination or lazy loading
The package includes comprehensive tests. Run them with:
composer testContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure tests pass (
composer test) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
The MIT License (MIT). Please see License File for more information.
- William Asaba
- Built with Laravel
- Powered by Livewire
- Styled with Tailwind CSS
- Enhanced with Alpine.js
If this package helped you, please ⭐ star the repository!

.png)