Range Group
A compact multi-range filter. One toggle summarises several numeric range filters inline
(Age / Salary / Children) and expands into a floating panel with one slider per
dimension — instead of spreading three separate sliders across a filter bar. Each row is dual-thumb
(min–max) or single-thumb (a ≥ / ≤ threshold), configured per-row.
In a filter bar
The range group drops into a .pa-filter-card__filters row as a single control standing in for three sliders. Open it, drag, and Apply.
Emitted values
The group dispatches pa-range-group:change live and pa-range-group:apply / :reset on the buttons. Payload is keyed by data-key; a bound at its extent reports null (i.e. “Any”).
// interact with the filter above…Single-thumb thresholds
Set mode: 'single' for a one-handle threshold. bound: 'gte' reads as
“value+”, bound: 'lte' as “≤ value”.
Bound values
{
"rating": {
"value": 4,
"bound": "gte"
},
"distance": {
"value": 20,
"bound": "lte"
}
}Handle shapes
Add a handleShape to any row to restyle its handles. Purely cosmetic (no JS), so you
can mix shapes per row. Open the filter to compare — circle (default), rect, bar, arrow, and needle (a downward
triangle balancing its point on the track).
Bound values
{
"circle": {
"min": 20,
"max": 70
},
"rect": {
"min": 20,
"max": 70
},
"bar": {
"min": 20,
"max": 70
},
"arrow": {
"min": 20,
"max": 70
},
"needle": {
"min": 20,
"max": 70
}
}Ticks & click-to-seek
Add ticks (major interval) and optionally ticksMinor to draw tick marks
along the track; add tickLabels to print the major values beneath. Marks sit behind the
track so only their ends show — major ticks reach further out than minor. On every row the track is click-to-seek: press anywhere off the handles and the nearest thumb jumps there and
keeps following the pointer. Add snapTicks to make the thumbs settle on the nearest tick
instead of the (possibly finer) step grid — drag the Level row and it snaps to
0, 10, 20…
Bound values
{
"age": {
"min": 25,
"max": 60
},
"score": {
"min": 40,
"max": 90
},
"level": {
"min": 20,
"max": 80
},
"rating": {
"value": 3,
"bound": "gte"
}
} <div class="pa-range" data-range data-key="age"
data-min="0" data-max="80"
data-ticks="20" data-ticks-minor="10" data-tick-labels>Theming with --pa-range-* tokens
Every colour and key dimension is a runtime token that falls back to the shared cascade, so a theme
or a per-instance panelStyle can retint/resize a slider with no recompile. This group
overrides the fill, thumb, track thickness, and handle size — set on the __panel (it
reparents to <body> when open, so tokens on the group root wouldn't reach the
sliders).
Bound values
{
"budget": {
"min": 1000,
"max": 3500
},
"guests": {
"value": 4,
"bound": "gte"
}
} <div class="pa-range-group__panel" data-range-group-panel
style="--pa-range-fill: #8b5cf6;
--pa-range-thumb-border: #8b5cf6;
--pa-range-track-height: 0.8rem;
--pa-range-thumb-size: 2rem;">Sync with the URL (qsKey)
Set qsKey and the group reads its initial state from that URL param and writes back
as you drag — bookmarkable, shareable, and back/forward-aware. The whole group packs into one
param via the built-in codec (override with codec). Only user changes are written,
so the URL starts empty; drag a slider and watch the address bar update. This demo also sets debounce={200}, so sliding back and forth coalesces into a single write when you
settle — the URL (and any fetch/filter you wire to it) never thrashes.
Live URL
(no query yet — drag a slider) Bound values (bind:values)
{} This docs site is SvelteKit, so it passes a ~10-line QsAdapter built on replaceState. A plain Vite / History app needs no adapter at all — just qsKey. For a hash SPA (@keenmate/svelte-spa-router), use the createSpaRouterAdapter factory. All three are below.
Usage
// 1 · Plain Vite / History app — zero config (default History adapter).
// debounce coalesces rapid drags into one write (values / onchange / URL):
<RangeGroup rows={filterRows} qsKey="filters" debounce={200} />
// 2 · SvelteKit — a tiny adapter: read from location, write via replaceState.
// subscribe MUST fire on external changes only (popstate) — NOT the page store,
// or a live write would remount the open panel.
import { replaceState } from '$app/navigation';
import type { QsAdapter } from '@keenmate/svelte-pure-admin';
const sveltekit: QsAdapter = {
read: (k) => new URLSearchParams(location.search).get(k),
write: (k, v) => {
const url = new URL(location.href);
if (v == null || v === '') url.searchParams.delete(k);
else url.searchParams.set(k, v);
replaceState(url.pathname + url.search, {});
},
subscribe: (cb) => {
addEventListener('popstate', cb);
return () => removeEventListener('popstate', cb);
}
};
<RangeGroup rows={filterRows} qsKey="filters" qsAdapter={sveltekit} />
// 3 · svelte-spa-router (hash SPA) — ready-made factory:
import { querystring, location, replace } from '@keenmate/svelte-spa-router';
import { createSpaRouterAdapter }
from '@keenmate/svelte-pure-admin/adapters/svelte-spa-router';
const router = createSpaRouterAdapter({ querystring, location, replace });
<RangeGroup rows={filterRows} qsKey="filters" qsAdapter={router} />Operation modes — mode="apply"
The URL demo above runs in the default immediate mode (live, debounced). Set mode="apply" to hold the committed state until the Apply button: bind:values (and any qsKey write) update only on Apply, while onchange keeps firing live — handy for an "N results" preview before committing.
Live onchange has fired 0 time(s) — but the
committed bind:values below only changes when you press Apply.
{} <!-- immediate (default): live as you drag, throttled with debounce -->
<RangeGroup {rows} bind:values debounce={200} />
<!-- apply: values / qsKey write commit only when Apply is pressed -->
<RangeGroup {rows} mode="apply" bind:values />Svelte usage
The <RangeGroup> wrapper renders the whole structure from a rows array. For the common case, bind:values gives you the live payload (keyed by key) with no event wiring; onchange / onapply / onreset remain for side effects, and qsKey (above) for URL sync.
<script lang="ts">
import { RangeGroup, type RangeGroupValues } from '@keenmate/svelte-pure-admin';
let values = $state<RangeGroupValues>({});
</script>
<!-- Two-way bound values — no event wiring needed -->
<RangeGroup
rows={[
{ key: 'age', label: 'Age', min: 18, max: 80, valueMin: 25, valueMax: 60 },
{ key: 'salary', label: 'Salary', min: 0, max: 200000, step: 5000,
valueMin: 40000, valueMax: 200000, prefix: '$', thousands: true },
{ key: 'children', label: 'Children', min: 0, max: 8,
mode: 'single', bound: 'gte', value: 2 }
]}
bind:values
/>
<!-- …or hook side effects instead (localStorage, fetch, …) -->
<RangeGroup {rows} onapply={(v) => save(v)} />Markup
One row per dimension. Positioning is driven in 0–100% via CSS custom properties on logical inset properties, so RTL mirrors automatically.
<div class="pa-range-group" data-range-group>
<button class="pa-range-group__toggle" data-range-group-toggle aria-expanded="false">
<!-- range-group.js fills this with "LABEL value / …" segments -->
<span class="pa-range-group__summary" data-range-group-summary></span>
<i class="fas fa-chevron-down pa-range-group__caret"></i>
</button>
<div class="pa-range-group__panel" data-range-group-panel>
<div class="pa-range-group__row">
<div class="pa-range-group__row-head">
<span class="pa-range-group__row-label">Age</span>
<span class="pa-range-group__row-value" data-range-output></span>
</div>
<div class="pa-range" data-range
data-key="age" data-min="18" data-max="80"
data-value-min="25" data-value-max="60">
<div class="pa-range__rail">
<div class="pa-range__track"></div>
<div class="pa-range__fill" data-range-fill></div>
<button class="pa-range__thumb pa-range__thumb--min" data-range-thumb="min"></button>
<button class="pa-range__thumb pa-range__thumb--max" data-range-thumb="max"></button>
</div>
</div>
</div>
<!-- …more rows… -->
<div class="pa-range-group__actions">
<button class="pa-btn pa-btn--sm pa-btn--ghost" data-range-group-reset>Reset</button>
<button class="pa-btn pa-btn--sm pa-btn--primary" data-range-group-apply>Apply</button>
</div>
</div>
</div>CSS Classes Reference
Compact control
pa-range-group- Root wrapper (toggle + floating panel)pa-range-group__toggle- The button summarising the filterspa-range-group__summary- Single-line "LABEL value / …" readout hostpa-range-group__seg-label- A dimension's label in the summarypa-range-group__seg-value- A dimension's value in the summarypa-range-group__seg-value--empty- Muted "Any" valuepa-range-group__seg-sep- The " / " separatorpa-range-group__caret- Dropdown chevron (rotates when open)pa-range-group--open- State: panel open (on the root)
Floating panel
pa-range-group__panel- The floating panel (reparented to body when open)pa-range-group__panel--open- State: shownpa-range-group__row- One dimension (head + slider)pa-range-group__row-head- Row label + value linepa-range-group__row-label- Row labelpa-range-group__row-value- Row value readoutpa-range-group__row-value--empty- Muted "Any" readoutpa-range-group__actions- Reset / Apply footer
Slider primitive
pa-range- Slider (dual-thumb by default)pa-range__rail- Inner rail the thumbs travel alongpa-range__track- Full trackpa-range__fill- Selected-range fillpa-range__thumb- A handlepa-range__thumb--min/--max- Low / high handlepa-range--single- Single-thumb (threshold) modepa-range--disabled- Non-interactive state
Handle shapes
pa-range--handle-rect- Rounded rectangle handlespa-range--handle-bar- Thin vertical bar handlespa-range--handle-arrow- Chevron handlespa-range--handle-needle- Downward-triangle "needle" handles
Tick marks
pa-range__ticks- Tick container (built by JS fromdata-ticks)pa-range__tick- A minor tick markpa-range__tick--major- A major tick mark (longer)pa-range__tick-labels- Container for the value labelspa-range__tick-label- A single major-tick labelpa-range--ticks-labeled- Row modifier reserving the label band
Theming tokens (CSS variables)
--pa-range-track/-fill- Track / fill colour--pa-range-thumb-bg/-thumb-border/-thumb-border-hover- Handle colours--pa-range-focus-ring- Thumb focus / active ring--pa-range-tick/-tick-major- Minor / major tick colour--pa-range-track-height/-thumb-size- Structural sizes--pa-range-group-panel-min-width- Floating panel width