jantinnerezo/livewire-range-slider
jantinnerezo/livewire-range-slider lets you easily add a configurable range slider to Livewire forms in Laravel. This lightweight Blade component supports single or dual handles, two-way model binding, min/max values, step sizes, and custom styling, with no extra JavaScript setup required.
Install and confirm compatibility first.
composer require jantinnerezo/livewire-range-slider
Before writing any markup, open vendor/jantinnerezo/livewire-range-slider/composer.json and check the require block. The last tagged release was June 2022, which predates Livewire 3 — if your app is on Livewire 3, treat this package as "evaluate, don't assume." On Livewire 2 it should drop in cleanly.
Check what the service provider does. The package is auto-discovered, but confirm whether it registers a config file, a view namespace, or a public asset. Look for a publishes() call in the provider:
php artisan vendor:publish --provider="Jantinnerezo\LivewireRangeSlider\LivewireRangeSliderServiceProvider"
Where to look first (in this order):
src/ — the Livewire component class. This tells you the exact public property name the slider binds to and whether the value is a scalar or an array.resources/views/ — the Blade template. Confirms prop names for min, max, step, and tooltip formatting.resources/js/ — the "no external JS" implementation. Verifies it's vanilla and how it dispatches updates back to Livewire.First use case — a price filter with two handles.
The dual-handle design almost always means the bound property is an array [min, max]. Declare it as such in your parent component:
// app/Http/Livewire/ProductSearch.php
class ProductSearch extends Component
{
public array $price = [0, 1000];
public function render()
{
return view('livewire.product-search', [
'products' => Product::whereBetween('price', $this->price)->get(),
]);
}
}
{{-- resources/views/livewire/product-search.blade.php --}}
<div>
<livewire:range-slider wire:model.live="price" :min="0" :max="1000" :step="10" />
<p>Showing ${{ $price[0] }} – ${{ $price[1] }}</p>
@foreach ($products as $product)
<div>{{ $product->name }}</div>
@endforeach
</div>
If the property shape doesn't match what the component emits (scalar vs array), Livewire will throw a TypeError on the first update — the source inspection above prevents this.
Choose your binding strategy deliberately.
| Binding | When to use |
|---|---|
wire:model.live |
Live filtering / instant results |
wire:model.live.debounce.500ms |
Live filtering where each drag tick would otherwise fire a request |
wire:model (default, deferred) |
Slider is part of a form submitted via a button |
For a range slider, deferred binding is a common trap: the handles move visually, but the server only learns about it on the next request. If your preview panel doesn't update, this is why.
Debounce aggressively — drag events are chatty.
<livewire:range-slider
wire:model.live.debounce.500ms="price"
:min="0"
:max="1000"
:step="50"
/>
Without debouncing, a single drag can produce dozens of round-trips and visibly stall the UI.
Keep filter state in the URL for shareable results.
protected $queryString = [
'price' => ['except' => [0, 1000]],
];
Note that query-string values arrive as strings; cast explicitly in mount() or a updatedPrice() hook if your comparisons are strict.
Reset pattern. A "Clear filters" button just reassigns the property:
<button wire:click="$set('price', [0, 1000])">Reset</button>
Reuse via a wrapper Blade component. Rather than repeating the tag with the same props everywhere, wrap it:
{{-- resources/views/components/price-filter.blade.php --}}
@props(['model' => 'price', 'max' => 1000])
<livewire:range-slider
wire:model.live.debounce.500ms="{{ $model }}"
:min="0"
:max="$max"
:step="10"
/>
Multiple sliders on one page. Give each a stable wire:key on the parent loop item; Livewire's DOM diffing can otherwise preserve the wrong slider state:
@foreach ($categories as $category)
<div wire:key="slider-{{ $category->id }}">
<livewire:range-slider wire:model.live="ranges.{{ $category->id }}" />
</div>
@endforeach
Validation belongs on the parent. The slider can emit values outside your business rules (e.g. min > max in edge cases), so validate in the parent's updatedPrice():
public function updatedPrice($value)
{
$this->validate(['price' => 'array|size:2', 'price.*' => 'numeric|min:0|max:1000']);
}
Tooltip formatting. The description advertises "formatted tooltips." Check the source for whether the prop is a format string, a closure, or a Blade slot — closures generally do not survive Livewire serialization, so if it's a closure-based formatter you'll need to pass a string pattern or precompute display text in the parent.
Integration with a filter form. Combine with selects/checkboxes; on each updated hook, re-query. Keep the query in a buildQuery() private method so the render stays readable as filters grow.
Livewire 3 compatibility is the big unknown. Last release 2022-06-11. Livewire 3 changed component naming, wire:model semantics, and asset injection. Before committing:
Livewire.find(...) / $wire in a way that survives the v3 refactor (v2-era packages often call window.livewire.emit or rely on wire:model internals).Property type must match exactly. If the source emits [min, max] and you declared public $price = 500, Livewire throws on the first update. Conversely, public array $price = [0, 1000] fails if the component emits a scalar string. Read the component class before wiring it up.
wire:model vs wire:model.live is the #1 debugging trap with this package. Symptom: handle moves, tooltip updates, but the server-side value or preview doesn't change. Fix: add .live (plus .debounce).
Numeric strings from query strings. After a page reload with ?price[0]=100&price[1]=500, values are strings. whereBetween usually tolerates this, but strict === comparisons and some validators will not. Cast in mount():
public function mount()
{
$this->price = array_map('intval', (array) $this->price);
}
Prop reactivity. :min/:max/:step are likely read once at component mount. If you need them dynamic (e.g. dependent on another filter), test whether they update — otherwise add wire:key to force a remount:
<livewire:range-slider
wire:key="slider-{{ $selectedCategory }}"
wire:model.live="price"
:min="0"
:max="$categoryMax"
/>
Assets. "No external JS" does not mean "no JS" — it means no jQuery/plugin dependency. Ensure @livewireScripts is present (v2) or that Livewire 3's auto-injection hasn't been disabled. If you use @livewireScripts(config: ['inject_assets' => false]), the slider will render but stay inert.
Styling overrides. Publish the views before customizing so you can see the class names:
php artisan vendor:publish --tag=livewire-range-slider-views
Remember that published views shadow the package on upgrade — diff them when you bump the (stale) version.
Debugging sync issues. In the browser devtools Network tab, inspect the Livewire update payload. If price isn't in updates, the JS never called $wire.set. If it is but the render didn't change, your render() is reading a stale or differently-shaped property. A quick ray($this->price) (or logger($this->price)) inside updatedPrice() confirms what the server actually receives.
Accessibility. Custom slider markup predating 2023 usually lacks role="slider", aria-valuemin/max/now, and keyboard support. If this is a user-facing filter, wrap it with an associated <label> and consider adding ARIA attributes in the published view.
Performance at scale. Use :step to reduce event frequency, and prefer .debounce.500ms over raw .live when the parent re-queries the database. For large result sets, combine with pagination so each update isn't a full table scan.
Extension points are limited. There's no plugin API documented; the practical extension mechanism is (a) publish and edit the view, or (b) copy the component into app/Livewire/ and own it. Given the package's age and low activity, option (b) is often the lower-risk choice for a production app.
How can I help you explore Laravel packages today?