Skip to content
ShineUI
Install

Slider

Drag a number into place.

<sui-slider>Advanced inputsStable

About

Track slider with one handle or a two-handle range and, with show-value, a formatted readout above the track. Drag a handle, click the track, or use the keyboard. Reach for it when a number has clear bounds and the exact figure matters less than where it sits in the range; use sui-input-number when it must be typed.

Usage

<sui-slider label="Late-fee grace period" name="grace" min="0" max="30" value="10"></sui-slider> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-slider"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiSlider } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiSlider label="Late-fee grace period" name="grace" min="0" max="30" value="10" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiSlider } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiSlider label="Late-fee grace period" name="grace" min="0" max="30" value="10" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiSlider } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiSlider], template: `<sui-slider label="Late-fee grace period" name="grace" min="0" max="30" value="10" (suiChange)="onChange($event)"></sui-slider>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Each handle is role="slider", and the formatted value is also its aria-valuetext; with show-value it is drawn at the end of the label row. A keyboard step fires suiInput and suiChange together.

<div style="max-width:420px"> <sui-slider label="Late-fee grace period" name="grace" min="0" max="30" value="10" step="1" show-value help="Days after the due date before a late fee applies."></sui-slider> </div>
// setup(root): properties and events setup(root) { root.querySelector('sui-slider').format = days; }

Quiet by default

Without show-value the slider draws the label, the track and the handle and says no number, which is the shape beside a field that shows the same figure, or for a level control. Add show-value where the figure is the point.

<div style="display:grid;gap:22px;max-width:420px"> <sui-slider label="Brightness" min="0" max="100" value="64"></sui-slider> <sui-slider label="Brightness" min="0" max="100" value="64" show-value></sui-slider> </div>

Range

Two handles on one track: they may meet but never cross, and a track press sends the nearer handle. The pair is value, as [low, high], so v-model and ngModel bind it. Screen readers hear "Amount filter minimum" and "Amount filter maximum".

<div style="max-width:420px"> <sui-slider id="sl-range" label="Amount filter" name="amount" range min="0" max="25000" step="500" show-value></sui-slider> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sl-range'); el.value = [2500, 18000]; el.format = money; }

Minimum gap

min-gap keeps the handles at least that far apart, in the slider's own units and rounded up to whole steps. Here the band is never narrower than $2,000.

<div style="max-width:420px"> <sui-slider id="sl-gap" label="Price band" name="band" range min="0" max="20000" step="500" min-gap="2000" show-value help="Drag the handles together to feel the gap."></sui-slider> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sl-gap'); el.value = [6000, 9000]; el.format = money; }

Vertical

orientation="vertical" stands the track up with the label above and, with show-value, the readout below; the host height sets the column height. ArrowUp raises the value.

<div style="display:flex;gap:28px;align-items:flex-start"> <sui-slider id="sl-v-bass" orientation="vertical" label="Bass" min="0" max="100" value="60" show-value style="height:200px"></sui-slider> <sui-slider id="sl-v-mid" orientation="vertical" label="Mid" min="0" max="100" value="45" show-value style="height:200px"></sui-slider> <sui-slider id="sl-v-treble" orientation="vertical" label="Treble" min="0" max="100" value="70" show-value style="height:200px"></sui-slider> <sui-slider id="sl-v-range" orientation="vertical" label="Comfort" range min="16" max="30" step="0.5" show-value style="height:200px"></sui-slider> </div>
// setup(root): properties and events setup(root) { ['#sl-v-bass', '#sl-v-mid', '#sl-v-treble'].forEach((sel) => { root.querySelector(sel).format = (v) => `${v}%`; }); const range = root.querySelector('#sl-v-range'); range.value = [19, 23]; range.format = (v) => `${v}°`; }

States

readonly keeps the handle focusable and readable and refuses every move. error carries a cross-field verdict and sets customError. A handle always sits somewhere, so required fails only while value is null: the slider shows no figure and holds the form until the person sets it.

<div style="display:grid;gap:22px;max-width:420px"> <sui-slider id="sl-req" label="Hours a week" name="hours" min="0" max="60" required show-value help="Set it to continue."></sui-slider> <sui-slider id="sl-ro" label="Approved retainer" min="0" max="10000" step="250" value="4000" readonly show-value help="Set when the engagement was approved."></sui-slider> <sui-slider id="sl-off" label="Write-off cap" min="0" max="5000" step="100" value="1500" disabled show-value></sui-slider> <sui-slider id="sl-err" label="Deposit" min="0" max="10000" step="250" value="500" show-value error="Deposit must be at least the retainer."></sui-slider> </div>
// setup(root): properties and events setup(root) { ['#sl-ro', '#sl-off', '#sl-err'].forEach((sel) => { root.querySelector(sel).format = money; }); root.querySelector('#sl-req').value = null; }

In a form

A single slider posts the number and a range posts "low,high". A disabled slider is barred from submission and posts nothing at all.

Submit to see what posts.
<form id="sl-form" style="display:grid;gap:22px;max-width:420px"> <sui-slider id="sl-form-grace" label="Grace period" name="grace" min="0" max="30" value="10" show-value></sui-slider> <sui-slider id="sl-form-band" label="Amount band" name="band" range min="0" max="25000" step="500" show-value></sui-slider> <sui-button type="submit" variant="brand" label="Submit"></sui-button> <output id="sl-form-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#sl-form'); const out = root.querySelector('#sl-form-out'); root.querySelector('#sl-form-grace').format = days; const band = root.querySelector('#sl-form-band'); band.value = [2500, 18000]; band.format = money; form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v}`).join(' '); }); }

Events

suiInput fires on every frame of a drag, suiChange once the drag is released or a keyboard step lands. Persist on suiChange, preview on suiInput.

Idle.
<div style="display:grid;gap:14px;max-width:420px"> <sui-slider id="sl-events" label="Write-off threshold" name="threshold" min="0" max="5000" step="50" value="1200" show-value></sui-slider> <span id="sl-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sl-events'); const log = root.querySelector('#sl-log'); let frames = 0; el.format = money; el.addEventListener('suiInput', () => { frames += 1; log.textContent = `suiInput fired ${frames} time(s).`; }); el.addEventListener('suiChange', (e) => { log.textContent = `suiChange ${e.detail} after ${frames} suiInput event(s).`; frames = 0; }); }

Keyboard

KeyAction
ArrowRight ArrowUpMoves the focused handle up one step, in both orientations. Under dir="rtl" a horizontal track runs from the right, so ArrowLeft raises the value and ArrowRight lowers it; the vertical keys never change.
ArrowLeft ArrowDownMoves it down one step.
PageUp PageDownMoves it ten steps.
Home EndJumps to the lowest or the highest value, or as far as the other handle and the gap allow.

API

<sui-slider>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables the track and its handles. A disabled slider posts nothing and never holds the form.
errorerrorstringstring | undefinedError message under the track; sets customError on the form and aria-invalid on the handles, for cross-field checks such as a deposit under the retainer.
formatproperty only((v: number) => string) | undefinedFormats a number for the readout and aria-valuetext, e.g. (v) => v + " days". Defaults to the value grouped and marked in the page's locale (1,250.5, 1.250,5). Property, not attribute.
helphelpstringstring | undefinedHelp text under the track. Replaced by error when set.
labellabelstringstring | undefinedLabel shown above the track, before the readout.
maxmaxnumber100Highest value.
minminnumber0Lowest value.
minGapmin-gapnumber0Smallest distance the two range handles keep between them, in the slider's own units and rounded up to a whole number of steps. A range set from outside that is already closer is left alone until a handle moves. 0 lets the handles meet.
namenamestringstring | undefinedForm control name. Posts the number, or low,high in range mode; a disabled slider posts nothing. Reflected: entry construction reads the content attribute, and the three framework bindings set props, so an unreflected name would post nothing.
orientationorientation"horizontal" | "vertical"'horizontal'Track direction. Vertical stands the track up with the label above and the readout below; ArrowUp raises the value in both. Set the height on the host, the default is 180px.
rangerangebooleanfalseTwo handles selecting a [low, high] pair in value instead of one number. The handles may meet but never cross, and a track press sends the nearer one.
readOnlyreadonlybooleanfalseDisplay only: no drag, no track click, no keyboard changes; the handles stay focusable and readable.
requiredrequiredbooleanfalseA value must be set for the surrounding form to be valid. A handle always sits somewhere, so this fails only while value is empty (null), which is how a host starts a slider the person must set, and what a framework form writes when it resets; the first drag, click or key sets it. A disabled or read-only slider never holds the form.
showValueshow-valuebooleanfalseDraws the formatted value, or both range values, at the end of the label row, and under the track when vertical. Off by default, so a slider beside a field that shows the same number says it once; add it where the figure is the point.
stepstepnumber1Increment between values; drag positions snap to it.
valuevalue[number, number] | null | number50The number, or with range the [low, high] pair. Mutable; updated before suiInput and suiChange fire, so v-model and ngModel bind either shape. The attribute takes a number; set a range's pair as a property. A value in the other mode's shape is rewritten to this mode's: a pair's low end for one handle, the whole track for a range given a single number. null is empty: the handles sit at the ends of the track, no figure shows, a screen reader hears "No value" for the handle, the slider posts an empty string, and with required it fails the form until the person sets it.

Events

EventDetailReactVueDescription
suiChangeSuiSliderValue[number, number] | numberonSuiChange@sui-changeFires when a drag ends or a keyboard step lands; detail is the number, or [low, high] in range mode.
suiInputSuiSliderValue[number, number] | numberonSuiInput@sui-inputFires continuously while a handle moves; detail is the number, or [low, high] in range mode.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the handle, or to the low handle of a range. sui-form calls this to reach the first invalid control.

Types

SuiSliderRange typeTwo-handle range value: [low, high].
No members.
SuiSliderValue typeThe slider's value: the number in single mode, the [low, high] pair in range mode.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also