This page features interactive demos and side-by-side code samples that need at least 340 pixels of horizontal space. Please widen your browser window, or rotate your device, to continue.
Component <neo-slider-range> is a two-handle slider picking a [min, max] range. The fill spans between the thumbs; each drags independently and is clamped against the other so the low handle can't pass the high handle. Clicking the rail moves the nearer thumb to the click point. Both endpoints have a contenteditable readout with the same number-only input filter as <neo-slider>.
Default
Decimal step
Dense mark labels
Negative range
Custom easing
Custom anchor + thumb
Vertical temperature
Bare rail (no header)
Disabled
DeactivateRenameDuplicateDelete
The preview routes through the in-browser server simulator. Its current Settings (network latency, a non-200 response, or an unreachable server) are in effect, so the live preview can look like it is misbehaving. Adjust them in the Settings panel.
New state Toggle autoplay
HTMLCSS
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.
<neo-slider-range
data-signals="{slider_range_value_min: '20', slider_range_value_max: '60'}"
min="0" max="100" step="1"
data-attr:value-min="$slider_range_value_min"
data-attr:value-max="$slider_range_value_max"
data-on:neo-slider-range-input="$slider_range_value_min = Number(evt.detail.min); $slider_range_value_max = Number(evt.detail.max)"
data-on:neo-slider-range-change="$slider_range_value_min = Number(evt.detail.min); $slider_range_value_max = Number(evt.detail.max)"
label="Volume" unit="%"
></neo-slider-range>
<neo-slider-range
min="0"
max="5"
step="0.1"
value-min="1.2"
value-max="3.8"
label="Gain"
unit="dB"
>
<span data-neo-slider-mark="0">−∞</span>
<span data-neo-slider-mark="1">1</span>
<span data-neo-slider-mark="2">2</span>
<span data-neo-slider-mark="3">3</span>
<span data-neo-slider-mark="4">4</span>
<span data-neo-slider-mark="5">5</span>
</neo-slider-range><neo-slider-range
min="0"
max="100"
step="1"
value-min="15"
value-max="85"
label="Project window"
unit="%"
>
<span data-neo-slider-mark="0">Backlog</span>
<span data-neo-slider-mark="20">Discovery</span>
<span data-neo-slider-mark="40">Definition</span>
<span data-neo-slider-mark="60">Development</span>
<span data-neo-slider-mark="80">Verification</span>
<span data-neo-slider-mark="100">Released</span>
</neo-slider-range><neo-slider-range
min="-50"
max="50"
step="1"
value-min="-15"
value-max="20"
label="Pan"
></neo-slider-range><neo-slider-range
min="0"
max="100"
step="1"
value-min="20"
value-max="70"
label="Brightness"
unit="%"
easing="320ms cubic-bezier(0.34, 1.35, 0.64, 1)"
></neo-slider-range><neo-slider-range
min="1"
max="5"
step="1"
value-min="2"
value-max="4"
label="Rating filter"
class="slider-range-stars-pg"
>
<span data-neo-slider-mark="1"></span>
<span data-neo-slider-mark="2"></span>
<span data-neo-slider-mark="3"></span>
<span data-neo-slider-mark="4"></span>
<span data-neo-slider-mark="5"></span>
<template data-neo-slider-anchor><neo-icon name="star"></neo-icon></template>
<template data-neo-slider-thumb>
<neo-icon name="star-filled"></neo-icon>
</template>
</neo-slider-range>/* neo-slider-range paints its two thumbs and rail anchors into a shadow
root; page CSS reaches them only through ::part(...) and the inherited
--neo-slider-* / --neo-icon-* custom properties. Both thumbs and every
dot clone the <template data-neo-slider-thumb|anchor> glyph, a <neo-icon>
that fills with the inherited color and sizes to --neo-icon-size. */
neo-slider-range.slider-range-stars-pg {
--neo-slider-anchor-size: 1.1rem;
--neo-slider-thumb-size: 1.4rem;
/* Drop the default dot fill and border so only the star glyph shows. */
--neo-slider-anchor-bg: transparent;
--neo-slider-anchor-border: transparent;
--neo-slider-anchor-active-bg: transparent;
--neo-slider-anchor-active-border: transparent;
}
/* Rail anchor: a muted outline star. margin-left + translateX re-center
every dot on its exact value; the component flushes the min/max dots
inward, which would leave an active dot peeking out beside a thumb. */
neo-slider-range.slider-range-stars-pg::part(anchor) {
display: grid;
place-items: center;
margin-left: 0 !important;
transform: translateX(-50%);
color: #9ca3af;
--neo-icon-size: 1.1rem;
transition: color 120ms ease;
}
/* Active anchor (a mark inside the selected [valueMin, valueMax] band):
a filled dot. The cloned outline glyph can't be filled from page CSS
(its SVG is a shadow boundary too deep), so the dot paints a solid
circle by masking a currentColor background; the circle stays small
enough to tuck inside the thumb star that covers the band's endpoints. */
neo-slider-range.slider-range-stars-pg::part(anchor active) {
color: var(--accent);
background-color: currentColor;
-webkit-mask: radial-gradient(circle at center, #000 0 0.3rem, #0000 0.32rem);
mask: radial-gradient(circle at center, #000 0 0.3rem, #0000 0.32rem);
}
/* Both drag thumbs: a larger filled accent star with no dot chrome. */
neo-slider-range.slider-range-stars-pg::part(thumb-min),
neo-slider-range.slider-range-stars-pg::part(thumb-max) {
display: grid;
place-items: center;
background: transparent;
border: none;
box-shadow: none;
color: var(--accent);
--neo-icon-size: 1.4rem;
cursor: grab;
}
neo-slider-range.slider-range-stars-pg::part(thumb-min):focus-visible,
neo-slider-range.slider-range-stars-pg::part(thumb-max):focus-visible {
box-shadow: 0 0 0 3px var(--neo-slider-focus-ring);
}
<neo-slider-range
min="-20"
max="40"
step="1"
value-min="5"
value-max="22"
label="Range"
unit="°C"
vertical
>
<span data-neo-slider-mark="-20">Cold</span>
<span data-neo-slider-mark="0">Freezing</span>
<span data-neo-slider-mark="20">Comfort</span>
<span data-neo-slider-mark="30">Warm</span>
<span data-neo-slider-mark="40">Hot</span>
</neo-slider-range><neo-slider-range
min="0"
max="100"
step="1"
value-min="35"
value-max="80"
hide-value
></neo-slider-range><neo-slider-range
min="0"
max="100"
step="1"
value-min="30"
value-max="65"
label="Locked"
unit="%"
disabled
></neo-slider-range>
step="0.1" snaps both values to one decimal place. The . key is allowed in the value fields because the step is non-integer. Marks show the dB scale's reference points; the active range lights up the marks it covers.
Labels that overlap or sit less than 1ch apart collapse automatically. Hover a hidden label to reveal it. When both active extremes compete for the same space, the label closest to either thumb stays visible. Equal distances prefer the high label; hover or focus the lower thumb to prefer the low label instead.
Set easing to animate the thumbs and fill between committed values. Click the rail or nudge with the keyboard to see the transition; dragging still tracks the pointer directly.
Set vertical to rotate the layout. The low value sits at the bottom and the high value at the top; pointer dragging follows the vertical rail. Marks light up when the active range covers them.
Omit label and set hide-value to drop the entire header row. The thumbs still focus on Tab and the value bubbles still pop on hover/press, so the control stays operable.
Like the single slider, a <template data-neo-slider-anchor> and <template data-neo-slider-thumb> are cloned into every dot and into both thumbs. Here the range picks a span of star ratings: each anchor is an outline <neo-icon name="star"> that becomes a filled accent dot while it sits inside the selected band, and both thumbs are filled stars. The cloned glyph is a shadow boundary too deep for page CSS to restyle, so an active anchor paints its dot from its own masked background.
A Datastar app can fat-morph the page at any moment, and a server narrowing or widening the band just re-emits the range with new value-min / value-max attributes. The rail, both thumbs, and the fill live in the component's shadow root, so a morph of the bare light host only changes those two attributes and never touches the thumb or fill nodes, so the easing transition eases the band to its new bounds. Autoplay cycles the states below, with no signals or morph hints, and the band glides from Low to Medium to High just the same.
Low
Medium
High
DeactivateRenameDuplicateDelete
The preview routes through the in-browser server simulator. Its current Settings (network latency, a non-200 response, or an unreachable server) are in effect, so the live preview can look like it is misbehaving. Adjust them in the Settings panel.
New state Toggle autoplay
HTMLCSS
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.