A powerful, feature-rich searchable dropdown component for Laravel Livewire 3 & 4 applications. Built with Alpine.js and styled with Tailwind CSS.
- Features
- Requirements
- Installation
- Tailwind CSS Setup
- Quick Start
- Component Props Reference
- Usage Examples
- Advanced Features
- Customization Guide
- Troubleshooting
- Performance Optimization
- Testing
- Demo Application
- Contributing
- License
- Real-time search - Client-side filtering as you type
- Multi-select support - Select multiple options with visual tags/badges
- Grouped options - Organize options into labeled categories
- Clear button - Quickly clear selections
- Dark mode support - Automatically adapts to your theme
- Accessible - Full keyboard navigation and ARIA attributes
- Livewire 3 & 4 compatible - Works seamlessly with both versions
- Responsive - Mobile-friendly and touch-optimized
- Disabled state - Conditional disabling support
- Flexible data - Works with Eloquent models, arrays, collections
- Dependent dropdowns - Perfect for cascading country → region → city selects
- Customizable - Override styles and behavior
- Zero config - Works immediately after installation
- PHP: 8.2 or higher
- Laravel: 11.x, 12.x, 13.x
- Livewire: 3.x or 4.x
- Alpine.js: Bundled with Livewire (no separate install needed)
- Tailwind CSS: 3.x or 4.x
Install the package via Composer:
composer require williamug/searchable-selectThe package will automatically register its service provider. You're ready to use it immediately!
You can publish the view files if you need to customize the component HTML:
php artisan vendor:publish --tag=searchable-select-viewsThe component is styled with Tailwind utility classes. You must tell Tailwind to scan the package's blade views, otherwise those classes will be purged and the dropdown will appear unstyled or invisible.
Add a @source directive to your main CSS file (typically resources/css/app.css):
@import'tailwindcss';
@source'../../vendor/williamug/searchable-select/resources/views/**/*.blade.php';Then rebuild your assets:
npm run buildAdd the package views to the content array in tailwind.config.js:
exportdefault{content: ['./resources/**/*.blade.php','./resources/**/*.js','./vendor/williamug/searchable-select/resources/views/**/*.blade.php',],theme: {extend: {},},plugins: [],}Then rebuild your assets:
npm run buildThat's it! The component will use Tailwind classes and support dark mode automatically.
Step 1: Create a Livewire Component
php artisan make:livewire ContactFormStep 2: Set up your component class
<?phpnamespaceApp\Livewire;
useApp\Models\Country;
useLivewire\Component;
class ContactForm extends Component
{
public$countries;
public$country_id;
publicfunctionmount()
{
// Load all countries$this->countries = Country::orderBy('name')->get();
}
publicfunctionsave()
{
$this->validate([
'country_id' => 'required|exists:countries,id',
]);
// Save your data...
}
publicfunctionrender()
{
returnview('livewire.contact-form');
}
}Step 3: Use the component in your Blade view
<div>
<labelfor="country"class="block mb-2">Country</label>
<x-searchable-selectwire:model="country_id":options="$countries"placeholder="Select a country"search-placeholder="Type to search countries..."
/>
@error('country_id')
<spanclass="text-red-500 text-sm mt-1">{{$message}}</span>
@enderror
<buttonwire:click="save"class="mt-4">Save</button>
</div>That's it! You now have a fully functional searchable dropdown. The component automatically syncs with your Livewire property via wire:model — no extra :selected-value prop needed.
Comprehensive list of all available props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
options | Array/Collection | [] | Yes | The list of options to display in the dropdown |
placeholder | String | 'Select option' | No | Placeholder text shown when nothing is selected |
searchPlaceholder | String | 'Search...' | No | Placeholder for the search input field |
disabled | Boolean | false | No | Whether the dropdown is disabled |
emptyMessage | String | 'No options available' | No | Message shown when the options array is empty |
optionValue | String | 'id' | No | The key/property to use as the option value |
optionLabel | String | 'name' | No | The key/property to use as the option display label |
multiple | Boolean | false | No | Enable multi-select mode (allows selecting multiple options) |
clearable | Boolean | true | No | Show/hide the clear button |
grouped | Boolean | false | No | Enable grouped/categorized options mode |
groupLabel | String | 'label' | No | Key for group labels (when grouped is true) |
groupOptions | String | 'options' | No | Key for group options array (when grouped is true) |
wire:modelis a standard Livewire directive, not a declared prop. Pass it aswire:model="propertyName"and the component handles two-way binding automatically.
options: The data source for your dropdown. Can be:- Eloquent Collection:
Country::all() - Array of objects:
[['id' => 1, 'name' => 'USA'], ...] - Array of arrays: See above
- Eloquent Collection:
wire:model: The Livewire property to bind to. The component uses$wire.entangle()internally to keep the selected value in sync automatically.
placeholder: Shows when no option is selectedsearchPlaceholder: Shows in the search inputemptyMessage: Shows whenoptionsarray is empty
optionValue: Which property to use as the value (saved towire:model)optionLabel: Which property to display to users
Example:
// If your model has 'code' and 'country_name' fields$countries = Country::all(); // [['code' => 'US', 'country_name' => 'United States'], ...]<x-searchable-selectwire:model="country_code":options="$countries"option-value="code"option-label="country_name"
/>multiple: Enables multi-select mode with visual tagsclearable: Shows/hides the × button to clear selectiondisabled: Grays out the component and prevents interactiongrouped: Enables category headers in the dropdown
The most common use case - a simple searchable dropdown:
<?phpnamespaceApp\Livewire;
useApp\Models\Country;
useLivewire\Component;
class UserProfile extends Component
{
public$countries;
public$country_id;
publicfunctionmount()
{
$this->countries = Country::orderBy('name')->get();
}
publicfunctionrender()
{
returnview('livewire.user-profile');
}
}<x-searchable-selectwire:model="country_id":options="$countries"placeholder="Select your country"search-placeholder="Search countries..."
/>Select multiple options with visual tags/badges:
<?phpnamespaceApp\Livewire;
useApp\Models\Skill;
useLivewire\Component;
class UserSkills extends Component
{
public$skills;
public$selected_skills = []; // Array to hold multiple selectionspublicfunctionmount()
{
$this->skills = Skill::orderBy('name')->get();
}
publicfunctionrender()
{
returnview('livewire.user-skills');
}
}<x-searchable-selectwire:model="selected_skills":options="$skills":multiple="true"placeholder="Select your skills"search-placeholder="Search skills..."
/>
{{-- Display selected skills --}}@if(!empty($selected_skills))
<divclass="mt-2">
<p>Selected: {{count($selected_skills) }} skills</p>
</div>
@endifSelected items show as blue badges with × remove buttons.
Create related dropdowns where child options depend on parent selections (e.g., Country → Region → City):
<?phpnamespaceApp\Livewire;
useApp\Models\{Country, Region, City};
useLivewire\Component;
class LocationSelector extends Component
{
// Optionspublic$countries;
public$regions = [];
public$cities = [];
// Selected valuespublic$country_id;
public$region_id;
public$city_id;
publicfunctionmount()
{
// Load countries on page load$this->countries = Country::orderBy('name')->get();
}
publicfunctionupdatedCountryId($value)
{
// When country changes, load its regions$this->regions = Region::where('country_id', $value)
->orderBy('name')
->get();
// Reset child selections$this->region_id = null;
$this->city_id = null;
$this->cities = [];
}
publicfunctionupdatedRegionId($value)
{
// When region changes, load its cities$this->cities = City::where('region_id', $value)
->orderBy('name')
->get();
// Reset city selection$this->city_id = null;
}
publicfunctionrender()
{
returnview('livewire.location-selector');
}
}<divclass="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Country Dropdown -->
<div>
<labelclass="block mb-2 font-medium">Country</label>
<x-searchable-selectwire:model="country_id":options="$countries"placeholder="Select Country"search-placeholder="Search countries..."
/>
</div>
<!-- Region Dropdown (disabled until country is selected) -->
<div>
<labelclass="block mb-2 font-medium">Region</label>
<x-searchable-selectwire:model="region_id":options="$regions":placeholder="empty($regions) ? 'First select a country' : 'Select Region'":disabled="!$country_id"
/>
</div>
<!-- City Dropdown (disabled until region is selected) -->
<div>
<labelclass="block mb-2 font-medium">City</label>
<x-searchable-selectwire:model="city_id":options="$cities":placeholder="empty($cities) ? 'First select a region' : 'Select City'":disabled="!$region_id"
/>
</div>
</div>Key points:
- Use
updatedPropertyName()methods in your Livewire component to react to changes —$wire.set()inside the component triggers these automatically on every selection - Reset child values when parent changes
- Use
:disabledprop to prevent selecting child before parent
Organize options into labeled categories:
<?phpnamespaceApp\Livewire;
useLivewire\Component;
class CountrySelector extends Component
{
public$country_id;
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'],
['id' => 7, 'name' => 'Spain'],
['id' => 8, 'name' => 'Italy'],
]
],
[
'label' => 'Asia',
'options' => [
['id' => 9, 'name' => 'Japan'],
['id' => 10, 'name' => 'China'],
['id' => 11, 'name' => 'India'],
['id' => 12, 'name' => 'South Korea'],
]
],
];
publicfunctionrender()
{
returnview('livewire.country-selector');
}
}<x-searchable-selectwire:model="country_id":options="$locations":grouped="true"placeholder="Select a country"search-placeholder="Search countries..."
/>Custom group keys:
If your data structure uses different keys:
public$categories = [
[
'category_name' => 'Fruits', // Custom group label key'items' => [ // Custom options key
['code' => 'APL', 'title' => 'Apple'],
['code' => 'BAN', 'title' => 'Banana'],
]
],
];<x-searchable-selectwire:model="selected_item":options="$categories":grouped="true"group-label="category_name"group-options="items"option-value="code"option-label="title"
/>When your data uses different property names:
public$products = [
['sku' => 'PROD-001', 'product_name' => 'Laptop'],
['sku' => 'PROD-002', 'product_name' => 'Mouse'],
['sku' => 'PROD-003', 'product_name' => 'Keyboard'],
];
public$selected_sku;<x-searchable-selectwire:model="selected_sku":options="$products"option-value="sku"option-label="product_name"placeholder="Select a product"
/>Integrate with Laravel's validation:
<?phpnamespaceApp\Livewire;
useApp\Models\Country;
useLivewire\Component;
class ContactForm extends Component
{
public$countries;
public$country_id;
public$city_id;
protected$rules = [
'country_id' => 'required|exists:countries,id',
'city_id' => 'required|exists:cities,id',
];
protected$messages = [
'country_id.required' => 'Please select a country.',
'city_id.required' => 'Please select a city.',
];
publicfunctionmount()
{
$this->countries = Country::all();
}
publicfunctionsave()
{
$validated = $this->validate();
// Use validated data...
}
publicfunctionrender()
{
returnview('livewire.contact-form');
}
}<div>
<label>Country *</label>
<x-searchable-selectwire:model="country_id":options="$countries"
/>
@error('country_id')
<spanclass="text-red-500 text-sm">{{$message}}</span>
@enderror
</div>
<divclass="mt-4">
<label>City *</label>
<x-searchable-selectwire:model="city_id":options="$cities"
/>
@error('city_id')
<spanclass="text-red-500 text-sm">{{$message}}</span>
@enderror
</div>
<buttonwire:click="save"class="mt-4">Save</button>Real-time validation:
publicfunctionupdated($propertyName)
{
$this->validateOnly($propertyName);
}Conditionally disable the dropdown:
<x-searchable-selectwire:model="region_id":options="$regions":disabled="!$country_id"placeholder="First select a country"
/>Hide the clear (×) button:
<x-searchable-selectwire:model="country_id":options="$countries":clearable="false"
/>You don't need Eloquent models - plain arrays work too:
public$statuses = [
['id' => 'draft', 'name' => 'Draft'],
['id' => 'published', 'name' => 'Published'],
['id' => 'archived', 'name' => 'Archived'],
];<x-searchable-selectwire:model="status":options="$statuses"
/>Add custom classes to the component wrapper:
<x-searchable-selectwire:model="country_id":options="$countries"class="border-2 border-blue-500 rounded-xl shadow-lg"
/>Build reusable components for common patterns:
resources/views/components/country-select.blade.php:
@props(['wireModel'])
<x-searchable-selectwire:model="{{$wireModel}}":options="\App\Models\Country::orderBy('name')->get()"placeholder="Select a country"search-placeholder="Search countries..."{{$attributes}}
/>Usage:
<x-country-selectwire-model="country_id" />For thousands of records, implement server-side search:
public$searchTerm = '';
public$countries = [];
publicfunctionupdatedSearchTerm($value)
{
$this->countries = Country::where('name', 'like', "%{$value}%")
->limit(50)
->get();
}If you need to customize the component HTML, publish the view file:
php artisan vendor:publish --tag=searchable-select-viewsThis copies the view to resources/views/vendor/searchable-select/searchable-select.blade.php. Laravel will use your copy instead of the package default.
The component automatically supports dark mode via Tailwind's dark: classes:
<htmlclass="dark"><!-- Component automatically uses dark:bg-zinc-800, dark:text-white, etc. --></html>The component uses client-side filtering by default. To customize:
- Case sensitivity: Modify the Alpine.js
searchTermfiltering logic in the published view - Search multiple fields: Adjust the filter to check multiple properties
- Server-side search: Use the
updatedSearchTermLivewire pattern shown in Advanced Features
Causes:
- Alpine.js not loaded
- JavaScript conflicts
- Multiple Alpine.js instances
Solutions:
- Verify Alpine.js is loaded (it comes with Livewire 3+):
@livewireScripts{{-- This includes Alpine.js --}}Check browser console for JavaScript errors (F12 → Console)
Ensure you're not loading Alpine.js separately if using Livewire 3+:
<!-- ❌ Remove this if you have Livewire 3+ --><scriptdefersrc="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>- Try clearing browser cache and hard refresh (Ctrl+Shift+R / Cmd+Shift+R)
Causes:
- Value mismatch between the Livewire property and options
- Wrong
optionValuekey - Value not in options array
Solutions:
- Verify the selected value exists in your options:
// ✅ Correct$this->country_id = 1;
$this->countries = Country::all(); // Contains id=1// ❌ Incorrect$this->country_id = 999; // ID doesn't exist in countries- Check
optionValuematches your data structure:
// If your data uses 'code' instead of 'id'$countries = [['code' => 'US', 'name' => 'USA']];<x-searchable-selectwire:model="country_code":options="$countries"option-value="code"{{-- Must specify 'code' --}}
/>- Use browser DevTools to inspect the component's Alpine.js data
Causes:
- Package views not included in Tailwind's content scanning
- Tailwind not rebuilt after adding the source
- CSS not loading
Solutions:
Add the package views to Tailwind's scan paths and rebuild:
Tailwind v4 — add a
@sourceline toresources/css/app.css:@source'../../vendor/williamug/searchable-select/resources/views/**/*.blade.php';
Tailwind v3 — add to the
contentarray intailwind.config.js:exportdefault{content: ['./resources/**/*.blade.php','./vendor/williamug/searchable-select/resources/views/**/*.blade.php',// Add this],}
Rebuild Tailwind CSS:
npm run build # or for development npm run devClear Laravel view cache:
php artisan view:clear
Check that your CSS is loading in browser DevTools (Network tab)
Causes:
- Missing
wire:keyon components in loops - Livewire not detecting changes
Solutions:
- Use
wire:keywhen rendering multiple components in loops:
@foreach($formsas$form)
<x-searchable-selectwire:key="country-{{$form->id}}"wire:model="forms.{{$loop->index}}.country_id":options="$countries"
/>
@endforeach- Every selection calls
$wire.set()immediately. Ensure your Livewire component has a matchingupdated{PropertyName}()method if you need to react to the change server-side.
Causes:
- Property not defined as array
- Missing
:multiple="true"
Solutions:
- Initialize property as array:
// ✅ Correctpublic$selected_items = [];
// ❌ Incorrectpublic$selected_items; // null, not an array- Enable multiple mode:
<x-searchable-select:multiple="true"{{-- Required for multi-select --}}wire:model="selected_items":options="$items"
/>Causes:
- Missing
@errordirective - Wrong property name in validation
Solutions:
- Add error display:
<x-searchable-selectwire:model="country_id":options="$countries" />
@error('country_id')
<spanclass="text-red-500 text-sm">{{$message}}</span>
@enderror- Verify property name matches:
// Componentpublic$country_id; // Property nameprotected$rules = [
'country_id' => 'required', // Must match property name
];Causes:
- Too many options loaded at once
- Client-side filtering thousands of items
Solutions:
- Use Livewire server-side search for large datasets:
public$searchTerm = '';
public$results = [];
publicfunctionupdatedSearchTerm($value)
{
$this->results = Product::where('name', 'like', "%{$value}%")
->limit(50)
->get();
}- Select only needed columns:
// ❌ Bad - loads all columns$this->users = User::all();
// ✅ Good - only id and name$this->users = User::select('id', 'name')->get();| Options Count | Recommended Approach |
|---|---|
| < 100 | Client-side filtering (default) - works perfectly |
| 100 - 1,000 | Client-side filtering with wire:key - still performant |
| 1,000+ | Server-side search with Livewire updated* hooks |
1. Server-Side Search:
// Livewire Componentpublic$searchTerm = '';
public$products = [];
publicfunctionupdatedSearchTerm($value)
{
$this->products = Product::where('name', 'like', "%{$value}%")
->limit(50)
->get();
}2. Caching Options:
publicfunctionmount()
{
$this->countries = Cache::remember('countries', 3600, function () {
return Country::orderBy('name')->get();
});
}3. Select Only Needed Columns:
// ❌ Bad - loads all columns$this->users = User::all();
// ✅ Good - only id and name$this->users = User::select('id', 'name')->get();The package includes a comprehensive test suite covering all features.
# Run all tests
composer test# Run with coverage
composer test -- --coverage
# Run specific test file
./vendor/bin/pest tests/Feature/ComponentTest.php
# Run tests in parallel
./vendor/bin/pest --parallelThe package tests include:
- Component rendering
- Single-select functionality
- Multi-select with badges/tags
- Grouped options rendering
- Service provider and component registration
- View namespace resolution
17 tests, 33 assertions - all passing
The package includes a full-featured demo application showcasing all features.
cd demo
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serveVisit http://localhost:8000
Note: The demo's
composer.jsonreferences the local package via a VCS repository pointing to../. No Packagist fetch needed — it installs directly from the local source.
The demo is a single consolidated page at / showcasing:
- Basic single-select
- Multi-select with badges
- Grouped options
- Preselected values
- Disabled state
Check the demo Livewire component in demo/app/Livewire/DemoPage.php for implementation examples.
See the Dependent/Cascading Dropdowns section for a complete example.
Yes! Publish the view file and edit your copy:
php artisan vendor:publish --tag=searchable-select-viewsYour copy lands in resources/views/vendor/searchable-select/searchable-select.blade.php.
Yes, fully compatible with both Livewire 3.x and 4.x.
Use a Livewire server-side search with a custom query:
publicfunctionupdatedSearchTerm($value)
{
$this->users = User::where('name', 'like', "%{$value}%")
->orWhere('email', 'like', "%{$value}%")
->orWhere('phone', 'like', "%{$value}%")
->get();
}Yes, initialize your property as an array:
public$selected_items = [1, 3, 5]; // Pre-selected IDsYes, the component automatically supports dark mode using Tailwind's dark: classes.
This feature is not built-in, but you can publish the view and add a disabled property check in the options loop.
The component is designed for Livewire. For Inertia.js, consider using a Vue/React select component instead.
Publish the view and customize the option rendering to include icons:
<div>
<imgsrc="{{$option->flag}}"class="w-4 h-4 inline mr-2">
{{$option->name}}
</div>We welcome contributions! Here's how to get started:
Fork the repository
git clone https://github.com/YOUR-USERNAME/searchable-select.git cd searchable-selectInstall dependencies
composer install
Run tests
composer test
Create a feature branch
git checkout -b feature/amazing-feature
Make your changes
- Add tests for new features
- Update documentation if needed
- Follow PSR-12 coding standards
Run tests and code style checks
composer test composer format # Fix code style
Commit your changes
git commit -m 'Add amazing feature'Push to your fork
git push origin feature/amazing-feature
Open a Pull Request
- Describe what your PR does
- Reference any related issues
- Ensure all tests pass
The project uses:
- Laravel Pint for PHP code formatting
- PSR-12 coding standard
- Pest PHP for testing
Run before committing:
composer format # Fix code style
composer test# Run test suiteFound a bug? Please open an issue with:
- Laravel version
- Livewire version
- PHP version
- Steps to reproduce
- Expected vs actual behavior
Have an idea? Open a feature request describing:
- The use case
- How it would work
- Why it's useful
- Any implementation ideas
Please see CHANGELOG for recent changes.
If you discover any security vulnerabilities, please email the maintainer instead of using the issue tracker.
- Laravel - The PHP Framework
- Livewire - A full-stack framework for Laravel
- Alpine.js - Your new, lightweight, JavaScript framework
- Tailwind CSS - A utility-first CSS framework
Inspired by the need for a simple, searchable select component for Laravel Livewire applications.
The MIT License (MIT). Please see License File for more information.
If this package saved you time and effort:
- ⭐ Star the repository on GitHub
- 🐦 Share it on social media
- 🤝 Contribute code or documentation
- 🐛 Report bugs to help improve it
- 💡 Suggest features you'd like to see
Your support helps maintain and improve this package!
- Packagist - Composer package
- GitHub Repository - Source code
- Issue Tracker - Report bugs or request features
- Changelog - Version history
- License - MIT License
Made with ❤️ for the Laravel community
If this package helped you, please ⭐ star the repository!

.png)