Slider

Sliders let people select a value from a range by dragging a handle along a track. Use one when the position in the range matters more than the exact number, as with volume, brightness or a price filter.

The component wraps a native <input type="range">, so keyboard support, form participation and the slider role come from the browser. The component handles pointer input itself, so a range slider grabs the nearer of its two handles.

Usage

value is bindable and defaults to 0. The range goes from min to max (0 to 100 by default).

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack">
	<Slider value={40} aria-label="Volume" />
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Variants

There are three variants. All use the same handle and gaps:

  • Standard fills from the start of the track to the handle.
  • Centered fills from the midpoint outward, so the handle shows an offset from neutral. Use it for balance, EQ or exposure correction.
  • Range has two handles and fills between them. Bind value and endValue. The handles cannot cross.
<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack">
	<Slider value={40} aria-label="Standard" />
	<Slider centered value={20} aria-label="Centered" />
	<Slider
		range
		value={30}
		endValue={70}
		aria-label="Minimum price"
		endInputAttributes={{ 'aria-label': 'Maximum price' }}
	/>
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Sizes

Five sizes, from xs to xl. The size sets the track height, the handle height and the track corner radius. Bigger tracks get proportionally less rounding, so an xl slider looks like a rounded rectangle, not a pill. The default is xs.

<script lang="ts">
	import { Slider } from 'noph-ui'

	const sizes: ('xs' | 's' | 'm' | 'l' | 'xl')[] = ['xs', 's', 'm', 'l', 'xl']
</script>

<div class="stack wide">
	{#each sizes as size (size)}
		<Slider {size} value={50} aria-label="Size {size}" />
	{/each}
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
	.wide {
		gap: 2.5rem;
	}
</style>
SizeTrack heightHandle heightTrack cornerIcon size
xs16dp44dp8dpn/a
s24dp44dp8dpn/a
m40dp44dp12dp24dp
l56dp68dp16dp24dp
xl96dp108dp28dp32dp

Orientation

orientation="vertical" runs the track bottom to top. The length comes from --np-slider-length (12rem by default). The native input uses the same writing mode, so ↑/↓ move the handle up and down.

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="row">
	<Slider orientation="vertical" value={60} aria-label="Brightness" />
	<Slider orientation="vertical" size="m" value={60} aria-label="Brightness" />
	<Slider
		orientation="vertical"
		size="l"
		--np-slider-length="9rem"
		value={60}
		aria-label="Brightness"
	/>
</div>

<style>
	.row {
		display: flex;
		align-items: flex-end;
		gap: 2.5rem;
	}
</style>

Steps and stop indicators

Set step to make the slider discrete. Add ticks to draw a stop indicator on every step. The tick under the handle is hidden, so the gap around the handle stays clear. At the two ends, the track's own stop indicators replace the ticks.

A discrete handle moves between the two track corners, not along the full track, so it lines up with the tick it selects.

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack">
	<Slider step={25} ticks value={50} min={0} max={100} aria-label="Rating" />
	<Slider step={10} ticks size="m" value={40} aria-label="Volume in tens" />
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Value indicator

labeled shows a value indicator above the handle while it is pressed, dragged or has keyboard focus. The container grows with its content, so format can return text of any length.

A continuous slider reports the exact position it was dragged to, which is rarely a good number to show. Pair labeled with a step, or round in format.

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack with-label">
	<Slider labeled step={1} value={40} aria-label="Volume" />
	<Slider labeled size="m" value={60} format={(v) => `${Math.round(v)}%`} aria-label="Brightness" />
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
	.with-label {
		gap: 3rem;
	}
</style>

Inset icon

The icon snippet is drawn inside the leading end of the active track. When the active track gets too short for it, at a low value or between the handles of a range slider, the icon moves to the inactive track and takes the inactive track's colour. The icon needs room, so use it with m and larger.

You render the snippet, so a slider at zero can show a different icon. Drag the volume slider below to the start and the icon changes to volume_off.

<script lang="ts">
	import { Icon } from 'noph-ui/icons'
	import { Slider } from 'noph-ui'

	let volume = $state(40)
</script>

<div class="stack">
	<Slider size="m" bind:value={volume} aria-label="Volume">
		{#snippet icon()}
			<Icon>{volume === 0 ? 'volume_off' : 'volume_up'}</Icon>
		{/snippet}
	</Slider>
	<Slider size="xl" value={60} aria-label="Brightness">
		{#snippet icon()}
			<Icon>light_mode</Icon>
		{/snippet}
	</Slider>
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Disabled

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack">
	<Slider disabled value={40} aria-label="Disabled" />
	<Slider disabled range value={20} endValue={70} aria-label="Disabled range" />
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Right-to-left

Horizontal sliders mirror in a right-to-left context, following the inherited dir. The track fills from the right and the stop indicator moves to the left edge.

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack" dir="rtl">
	<Slider value={60} aria-label="Volume, right to left" />
	<Slider range value={30} endValue={70} aria-label="Price, right to left" />
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Theming

TokenDefault value
--np-slider-active-track-color--np-color-primary
--np-slider-inactive-track-color--np-color-secondary-container
--np-slider-handle-color--np-color-primary
--np-slider-active-stop-color--np-color-on-primary
--np-slider-inactive-stop-color--np-color-on-secondary-container
--np-slider-label-container-color--np-color-inverse-surface
--np-slider-label-text-color--np-color-inverse-on-surface
--np-slider-track-heightper size, 1rem at xs
--np-slider-track-shapeper size, 0.5rem at xs
--np-slider-track-inside-shape0.125rem
--np-slider-handle-heightper size, 2.75rem at xs
--np-slider-handle-width0.25rem
--np-slider-handle-shape--np-shape-corner-full
--np-slider-stop-indicator-size0.25rem
--np-slider-icon-sizeper size, 1.5rem at m
--np-slider-length12rem, vertical only
--np-slider-icon-color--np-color-on-primary
--np-slider-icon-inactive-color--np-color-on-secondary-container
--np-slider-icon-padding0.625rem
--np-slider-handle-width-focus0.125rem
--np-slider-disabled-active-track-color--np-color-on-surface
--np-slider-disabled-inactive-track-color--np-color-on-surface
--np-slider-disabled-handle-color--np-color-on-surface

The handle narrows to --np-slider-handle-width-focus while it is pressed or focused. Its height stays the same. The disabled colours get their own opacity, so set a full colour, not a faded one.

Example

<script lang="ts">
	import { Slider } from 'noph-ui'
</script>

<div class="stack">
	<Slider
		--np-slider-active-track-color="var(--np-color-error)"
		--np-slider-inactive-track-color="var(--np-color-error-container)"
		--np-slider-handle-color="var(--np-color-error)"
		--np-slider-track-shape="0.25rem"
		value={40}
		aria-label="Slider custom"
	/>
</div>

<style>
	.stack {
		display: flex;
		flex-direction: column;
		gap: 2rem;
		min-width: min(22rem, 70vw);
	}
</style>

Accessibility

The slider has no text, so pass an aria-label (or aria-labelledby) that says what it controls. A range slider has two inputs. The spread attributes go to the start handle, and endInputAttributes go to the end handle. Label both.

Arrow keys move by one step, Home/End jump to the ends, and Page Up/Page Down move in larger steps. This comes from the native input.

A format function also sets aria-valuetext, so a screen reader says 21 percent instead of 21. Use it when the number needs a unit.

API

Attributes

Anything not listed here is forwarded to the underlying <input type="range">.

AttributeTypeDefaultDescription
minnumber0Lowest selectable value.
maxnumber100Highest selectable value.
stepnumber0Step increment. 0 makes the slider continuous.
size'xs' | 's' | 'm' | 'l' | 'xl''xs'Track height, handle height and corner radius.
orientation'horizontal' | 'vertical''horizontal'Vertical sliders run bottom to top.
rangebooleanfalseTwo handles selecting a sub-range. Bind value and endValue.
centeredbooleanfalseFills the active track from the midpoint instead of the start.
labeledbooleanfalseShows the value indicator on press, drag and keyboard focus.
ticksbooleanfalseDraws a stop indicator on every step. Requires step.
disabledbooleanfalseDisables the slider.
format(value: number) => stringStringFormats the value indicator.
iconSnippetundefinedIcon drawn inside the leading end of the active track.
endInputAttributesHTMLInputAttributesundefinedAttributes for the end handle's input. range only.

Bindables

AttributeTypeDescription
valuenumberCurrent value. With range, the start handle's value.
endValuenumber | undefinedEnd handle's value. range only. Defaults to max.
elementHTMLDivElementThe slider's root element.
inputElementHTMLInputElementThe start handle's input.
endInputElementHTMLInputElementThe end handle's input. range only.