Skip to content

Repository files navigation

Livewire Searchable Select

Latest Version on Packagistrun-testsGitHub Code Style Action StatusTotal DownloadsLicense

A powerful, feature-rich searchable dropdown component for Laravel Livewire 3 & 4 applications. Built with Alpine.js and styled with Tailwind CSS.

Table of Contents

Features

  • 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

Screenshots

Requirements

  • 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

Installation

Install the package via Composer:

composer require williamug/searchable-select

The 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-views

Tailwind CSS Setup

The 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.

Tailwind v4 (@tailwindcss/vite or @tailwindcss/postcss)

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 build

Tailwind v3 (tailwindcss with tailwind.config.js)

Add 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 build

That's it! The component will use Tailwind classes and support dark mode automatically.

Quick Start

Basic Usage

Step 1: Create a Livewire Component

php artisan make:livewire ContactForm

Step 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.

Component Props Reference

Comprehensive list of all available props:

PropTypeDefaultRequiredDescription
optionsArray/Collection[]YesThe list of options to display in the dropdown
placeholderString'Select option'NoPlaceholder text shown when nothing is selected
searchPlaceholderString'Search...'NoPlaceholder for the search input field
disabledBooleanfalseNoWhether the dropdown is disabled
emptyMessageString'No options available'NoMessage shown when the options array is empty
optionValueString'id'NoThe key/property to use as the option value
optionLabelString'name'NoThe key/property to use as the option display label
multipleBooleanfalseNoEnable multi-select mode (allows selecting multiple options)
clearableBooleantrueNoShow/hide the clear button
groupedBooleanfalseNoEnable grouped/categorized options mode
groupLabelString'label'NoKey for group labels (when grouped is true)
groupOptionsString'options'NoKey for group options array (when grouped is true)

wire:model is a standard Livewire directive, not a declared prop. Pass it as wire:model="propertyName" and the component handles two-way binding automatically.

Props Explanation

Core Props

  • 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
  • wire:model: The Livewire property to bind to. The component uses $wire.entangle() internally to keep the selected value in sync automatically.

Labeling Props

  • placeholder: Shows when no option is selected
  • searchPlaceholder: Shows in the search input
  • emptyMessage: Shows when options array is empty

Data Mapping Props

  • optionValue: Which property to use as the value (saved to wire: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"
/>

Feature Flags

  • multiple: Enables multi-select mode with visual tags
  • clearable: Shows/hides the × button to clear selection
  • disabled: Grays out the component and prevents interaction
  • grouped: Enables category headers in the dropdown

Usage Examples

Basic Single Select

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..."
/>

Multi-Select

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>
@endif

Selected items show as blue badges with × remove buttons.

Dependent/Cascading Dropdowns

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 :disabled prop to prevent selecting child before parent

Grouped Options

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"
/>

Custom Keys

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"
/>

With Validation

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);
}

Disabled State

Conditionally disable the dropdown:

<x-searchable-selectwire:model="region_id":options="$regions":disabled="!$country_id"placeholder="First select a country"
/>

Without Clear Button

Hide the clear (×) button:

<x-searchable-selectwire:model="country_id":options="$countries":clearable="false"
/>

Using Arrays Instead of Models

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"
/>

Advanced Features

Custom Styling with CSS Classes

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"
/>

Creating Specialized Components

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" />

Server-Side Search (Large Datasets)

For thousands of records, implement server-side search:

public$searchTerm = '';
public$countries = [];
publicfunctionupdatedSearchTerm($value)
{
$this->countries = Country::where('name', 'like', "%{$value}%")
->limit(50)
->get();
}

Customization Guide

Publishing Views

If you need to customize the component HTML, publish the view file:

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

This copies the view to resources/views/vendor/searchable-select/searchable-select.blade.php. Laravel will use your copy instead of the package default.

Dark Mode Support

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>

Customizing Search Behavior

The component uses client-side filtering by default. To customize:

  1. Case sensitivity: Modify the Alpine.js searchTerm filtering logic in the published view
  2. Search multiple fields: Adjust the filter to check multiple properties
  3. Server-side search: Use the updatedSearchTerm Livewire pattern shown in Advanced Features

Troubleshooting

Common Issues and Solutions

Dropdown doesn't open / Click doesn't work

Causes:

  • Alpine.js not loaded
  • JavaScript conflicts
  • Multiple Alpine.js instances

Solutions:

  1. Verify Alpine.js is loaded (it comes with Livewire 3+):
@livewireScripts{{-- This includes Alpine.js --}}
  1. Check browser console for JavaScript errors (F12 → Console)

  2. 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>
  1. Try clearing browser cache and hard refresh (Ctrl+Shift+R / Cmd+Shift+R)

Selected value not displaying

Causes:

  • Value mismatch between the Livewire property and options
  • Wrong optionValue key
  • Value not in options array

Solutions:

  1. 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
  1. Check optionValue matches 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' --}}
/>
  1. Use browser DevTools to inspect the component's Alpine.js data

Styling issues / dropdown appears unstyled or invisible

Causes:

  • Package views not included in Tailwind's content scanning
  • Tailwind not rebuilt after adding the source
  • CSS not loading

Solutions:

  1. Add the package views to Tailwind's scan paths and rebuild:

    Tailwind v4 — add a @source line to resources/css/app.css:

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

    Tailwind v3 — add to the content array in tailwind.config.js:

    exportdefault{content: ['./resources/**/*.blade.php','./vendor/williamug/searchable-select/resources/views/**/*.blade.php',// Add this],}
  2. Rebuild Tailwind CSS:

    npm run build
    # or for development
    npm run dev
  3. Clear Laravel view cache:

    php artisan view:clear
  4. Check that your CSS is loading in browser DevTools (Network tab)

Options not updating / Stale data

Causes:

  • Missing wire:key on components in loops
  • Livewire not detecting changes

Solutions:

  1. Use wire:key when 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
  1. Every selection calls $wire.set() immediately. Ensure your Livewire component has a matching updated{PropertyName}() method if you need to react to the change server-side.

Multi-select not working

Causes:

  • Property not defined as array
  • Missing :multiple="true"

Solutions:

  1. Initialize property as array:
// ✅ Correctpublic$selected_items = [];
// ❌ Incorrectpublic$selected_items; // null, not an array
  1. Enable multiple mode:
<x-searchable-select:multiple="true"{{-- Required for multi-select --}}wire:model="selected_items":options="$items"
/>

Validation errors not showing

Causes:

  • Missing @error directive
  • Wrong property name in validation

Solutions:

  1. Add error display:
<x-searchable-selectwire:model="country_id":options="$countries" />
@error('country_id')
<spanclass="text-red-500 text-sm">{{$message}}</span>
@enderror
  1. Verify property name matches:
// Componentpublic$country_id; // Property nameprotected$rules = [
'country_id' => 'required', // Must match property name
];

Performance issues with large datasets

Causes:

  • Too many options loaded at once
  • Client-side filtering thousands of items

Solutions:

  1. Use Livewire server-side search for large datasets:
public$searchTerm = '';
public$results = [];
publicfunctionupdatedSearchTerm($value)
{
$this->results = Product::where('name', 'like', "%{$value}%")
->limit(50)
->get();
}
  1. Select only needed columns:
// ❌ Bad - loads all columns$this->users = User::all();
// ✅ Good - only id and name$this->users = User::select('id', 'name')->get();

Performance Optimization

Dataset Size Guidelines

Options CountRecommended Approach
< 100Client-side filtering (default) - works perfectly
100 - 1,000Client-side filtering with wire:key - still performant
1,000+Server-side search with Livewire updated* hooks

Optimization Techniques

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();

Testing

The package includes a comprehensive test suite covering all features.

Running Tests

# 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 --parallel

Test Coverage

The 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

Demo Application

The package includes a full-featured demo application showcasing all features.

Running the Demo

cd demo
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serve

Visit http://localhost:8000

Note: The demo's composer.json references the local package via a VCS repository pointing to ../. No Packagist fetch needed — it installs directly from the local source.

Demo Features

The demo is a single consolidated page at / showcasing:

  • Basic single-select
  • Multi-select with badges
  • Grouped options
  • Preselected values
  • Disabled state

Demo Source Code

Check the demo Livewire component in demo/app/Livewire/DemoPage.php for implementation examples.

Frequently Asked Questions

How do I implement country → state → city dropdowns?

See the Dependent/Cascading Dropdowns section for a complete example.

Can I customize the component HTML?

Yes! Publish the view file and edit your copy:

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

Your copy lands in resources/views/vendor/searchable-select/searchable-select.blade.php.

Does it work with Livewire 3 and 4?

Yes, fully compatible with both Livewire 3.x and 4.x.

How do I search across multiple fields?

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();
}

Can I pre-select multiple values?

Yes, initialize your property as an array:

public$selected_items = [1, 3, 5]; // Pre-selected IDs

Does it support dark mode?

Yes, the component automatically supports dark mode using Tailwind's dark: classes.

How do I disable specific options?

This feature is not built-in, but you can publish the view and add a disabled property check in the options loop.

Can I use it with Inertia.js?

The component is designed for Livewire. For Inertia.js, consider using a Vue/React select component instead.

How do I add icons to options?

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>

Contributing

We welcome contributions! Here's how to get started:

Development Setup

  1. Fork the repository

    git clone https://github.com/YOUR-USERNAME/searchable-select.git
    cd searchable-select
  2. Install dependencies

    composer install
  3. Run tests

    composer test

Contribution Workflow

  1. Create a feature branch

    git checkout -b feature/amazing-feature
  2. Make your changes

    • Add tests for new features
    • Update documentation if needed
    • Follow PSR-12 coding standards
  3. Run tests and code style checks

    composer test
    composer format # Fix code style
  4. Commit your changes

    git commit -m 'Add amazing feature'
  5. Push to your fork

    git push origin feature/amazing-feature
  6. Open a Pull Request

    • Describe what your PR does
    • Reference any related issues
    • Ensure all tests pass

Code Style

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 suite

Reporting Bugs

Found a bug? Please open an issue with:

  • Laravel version
  • Livewire version
  • PHP version
  • Steps to reproduce
  • Expected vs actual behavior

Suggesting Features

Have an idea? Open a feature request describing:

  • The use case
  • How it would work
  • Why it's useful
  • Any implementation ideas

Changelog

Please see CHANGELOG for recent changes.

Security

If you discover any security vulnerabilities, please email the maintainer instead of using the issue tracker.

Credits

Author

Built With

Inspiration

Inspired by the need for a simple, searchable select component for Laravel Livewire applications.

License

The MIT License (MIT). Please see License File for more information.

Support the Project

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!

Links


Made with ❤️ for the Laravel community

If this package helped you, please ⭐ star the repository!

Report Bug · Request Feature · Contribute

About

A powerful, feature-rich searchable dropdown component for Laravel Livewire 3 & 4 applications. Built with Alpine.js and styled with Tailwind CSS.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages