Slider
Selection & Inputimport { Slider } from "bits-ui" Single value
A numeric value updates continuously as the thumb moves
<script lang="ts">
import { Slider } from 'bits-ui';
let value = $state(62);
</script>
<div class="grid w-full max-w-md gap-5">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Output volume</span>
<output class="chip font-mono">{value}%</output>
</div>
<Slider.Root
type="single"
bind:value
min={0}
max={100}
step={1}
class="relative flex w-full touch-none items-center select-none"
trackPadding={2}
>
<span class="relative h-2 w-full grow overflow-hidden rounded-full bg-ink-800">
<Slider.Range class="absolute h-full rounded-full bg-brand-500" />
</span>
<Slider.Thumb
index={0}
aria-label="Output volume"
class="block size-5 cursor-grab rounded-full border-2 border-brand-400 bg-ink-950 shadow-lg outline-none transition-transform data-active:scale-110 data-active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-brand-400 focus-visible:ring-offset-2 focus-visible:ring-offset-ink-900"
/>
</Slider.Root>
<div class="flex justify-between text-xs text-ink-400"><span>Quiet</span><span>Full</span></div>
</div>
Range
type="multiple" binds number[]; the thumbs snippet supplies the indexes to render
Drag either thumb; values stay sorted as the handles cross.
<script lang="ts">
import { Slider } from 'bits-ui';
let value = $state<number[]>([25, 70]);
const lower = $derived(value[0] ?? 0);
const upper = $derived(value[1] ?? 0);
</script>
<div class="grid w-full max-w-md gap-7">
<div class="flex items-baseline justify-between gap-3">
<span class="text-sm font-medium">Monthly infrastructure budget</span>
<output class="chip font-mono">${lower}k–${upper}k</output>
</div>
<Slider.Root
type="multiple"
bind:value
min={0}
max={100}
step={5}
class="relative flex w-full touch-none items-center select-none"
trackPadding={2}
>
{#snippet children({ thumbs })}
<span class="relative h-2 w-full grow overflow-hidden rounded-full bg-ink-800">
<Slider.Range class="absolute h-full rounded-full bg-brand-500" />
</span>
{#each thumbs as index (index)}
<Slider.Thumb
{index}
aria-label={index === 0 ? 'Minimum budget' : 'Maximum budget'}
class="block size-5 cursor-grab rounded-full border-2 border-brand-400 bg-ink-950 shadow-lg outline-none transition-transform data-active:scale-110 data-active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-brand-400 focus-visible:ring-offset-2 focus-visible:ring-offset-ink-900"
/>
<Slider.ThumbLabel
{index}
position="top"
class="mb-3 rounded-md bg-ink-800 px-1.5 py-0.5 font-mono text-xs text-ink-200"
>
${value[index]}k
</Slider.ThumbLabel>
{/each}
{/snippet}
</Slider.Root>
<p class="muted text-xs">Drag either thumb; values stay sorted as the handles cross.</p>
</div>
Discrete steps and ticks
An array-valued step prop creates a non-linear scale, with thumbs, ticks and tickItems supplied to the children snippet
Editor font size
Discrete scale, not a continuous interval
<script lang="ts">
import { Slider } from 'bits-ui';
const steps = [8, 12, 16, 24, 32, 48];
let value = $state(16);
</script>
<div class="grid w-full max-w-lg gap-8">
<div class="flex items-baseline justify-between gap-3">
<div>
<p class="text-sm font-medium">Editor font size</p>
<p class="muted text-xs">Discrete scale, not a continuous interval</p>
</div>
<output class="chip font-mono">{value}px</output>
</div>
<Slider.Root
type="single"
bind:value
step={steps}
trackPadding={3}
class="relative flex w-full touch-none items-center pb-7 select-none"
>
{#snippet children({ thumbs, ticks, tickItems })}
<span class="relative h-2 w-full grow overflow-hidden rounded-full bg-ink-800">
<Slider.Range class="absolute h-full rounded-full bg-accent-500" />
</span>
{#each ticks as index (index)}
<Slider.Tick
{index}
class="z-10 h-2 w-px bg-ink-600 data-bounded:bg-ink-950 data-selected:w-0.5 data-selected:bg-ink-50"
/>
<Slider.TickLabel
{index}
position="bottom"
class="mt-3 -translate-x-1/2 font-mono text-[0.68rem] text-ink-400 data-selected:text-accent-500"
>
{tickItems[index]?.value}
</Slider.TickLabel>
{/each}
{#each thumbs as index (index)}
<Slider.Thumb
{index}
aria-label="Editor font size"
class="z-20 block size-5 cursor-grab rounded-full border-2 border-accent-500 bg-ink-950 shadow-lg outline-none transition-transform data-active:scale-110 data-active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-2 focus-visible:ring-offset-ink-900"
/>
{/each}
{/snippet}
</Slider.Root>
</div>
type is required: single mode binds a number; multiple mode binds a number array and automatically sorts crossed thumbs unless autoSort={false}. The root children snippet exposes thumb indexes and tick indexes; tickItems adds each tick’s numeric value. An array passed to step defines discrete allowed values rather than a uniform increment.
Slider deliberately renders no hidden form input because its value space is effectively unbounded. Add your own hidden input for a single value, or one input per range endpoint. Use onValueCommit for expensive work after a drag; onValueChange runs throughout it.