Vanilla UI Kit Slider v1.0.0 · tabs →

Sliders, one file.

Single or dual thumb, marks, tooltips, vertical, and real form participation — zero dependencies, full keyboard and screen-reader support. Drag anything below.

Basic

Drag the thumb, or press the track to jump. Arrow keys work too.

40%
new Slider('#basic', {
  value: 40, suffix: '%',
  onInput: v => show(v)
})

Price range

Dual thumb with $ formatting and always-on bubbles. Thumbs clamp at each other.

$200 – $650
new Slider('#price', {
  min: 0, max: 1000, step: 10,
  value: [200, 650],
  prefix: '$', tooltip: 'always'
})

Steps + marks

Snaps to the step grid; labeled marks under the rail.

16 GB
new Slider('#ram', {
  min: 0, max: 64, step: 16, value: 16,
  marks: { 0:'0', 16:'16', 32:'32', 48:'48', 64:'64 GB' },
  format: v => v + ' GB'
})

Vertical

vertical: true — bottom to top; ArrowUp still increases.

vol 65 · 20°C
new Slider('#vol',  { value: 65, vertical: true })
new Slider('#temp', { min: -10, max: 40, value: 20,
  vertical: true, suffix: '°C', tooltip: 'always' })

Replace a native range

The <input> is hidden and kept synced — submit posts real values (dual adds budget[] hidden inputs).

— submit to see the payload —
new Slider(document.getElementById('native-volume'))
new Slider('#budget', { name: 'budget', prefix: '$',
  max: 500, value: [100, 350] })

Disabled

disabled: true, toggled live via enable()/disable().

var s = new Slider('#locked',
  { value: 70, disabled: true })
s.enable(); s.disable()

API reference

Constructor & usage

new Slider(target, options?) — target is a CSS selector string or an element. A non-matching target throws; constructing on an element that already holds an instance destroys the old instance first. Without a DOM (SSR), the constructor returns an inert no-op instance.

Build mode — pass an empty container and the slider is built inside it. With name set, hidden input(s) are created so plain form posts work:

<div id="volume"></div>
<script>
  var volume = new Slider('#volume', { value: 40, suffix: '%' })
</script>

Dual thumb — a two-element array as value creates a range with two thumbs that clamp at each other:

new Slider('#price', {
  min: 0, max: 1000, step: 10,
  value: [200, 650],          // [lo, hi] — sorted, snapped, clamped
  prefix: '$'
})

Replace mode (progressive enhancement) — pass an <input> and it is hidden (via the hidden attribute) and kept synced, so the form keeps posting the real value ("20,80" when dual) and receives native input / change events. The input's own min, max, step, value and disabled attributes seed any omitted options. destroy() restores the input untouched:

<input type="range" name="volume" min="0" max="100" value="40">
<script>
  new Slider(document.querySelector('[name=volume]'))
</script>

Static helpers cover the same ground:

Slider.create('#el', opts)   // identical to new Slider('#el', opts)
Slider.get('#el')            // existing instance for an element, or null
Slider.autoInit()            // init every [data-vsld] — also runs on page load

Options

All keys of Slider.defaults, with their exact defaults. Unknown option keys are ignored; there are no per-call-only constructor options beyond these. In replace mode, omitted min / max / step / value / disabled are seeded from the replaced input's attributes.

OptionTypeDefaultDescription
minnumber0 Lower bound. Non-numeric values fall back to 0; if max < min the two are swapped.
maxnumber100 Upper bound. Non-numeric values fall back to 100.
stepnumber1 Snap grid, anchored at min. Values not > 0 fall back to 1.
valuenumber | [number, number] | nullnull Number = single thumb; two-element array = dual-thumb range (sorted, snapped, clamped); null starts at min.
marksboolean | objectfalse true = one tick per step, only when the range spans 1–20 steps (otherwise no ticks). An object like { 0: 'Low', 50: 'Mid', 100: 'High' } places labeled ticks at those values; keys outside [min, max] are ignored.
tooltip'drag' | 'always' | false'drag' 'drag' shows the value bubble while dragging or keyboard-focused; 'always' keeps it visible; false renders no bubble at all.
formatfunction | nullnull fn(value) → string used for the tooltip and aria-valuetext. When set, prefix/suffix are ignored.
prefixstring'' Shorthand formatting when no format is given: prefix + value + suffix.
suffixstring'' See prefix.
verticalbooleanfalse Rail runs bottom → top. ArrowUp still increases the value.
disabledbooleanfalse Start disabled: thumbs unfocusable, form control(s) get the disabled attribute so nothing submits.
namestring | nullnull Build mode only: creates hidden input(s) — name for a single value, name[] twice for dual (the [] is appended unless already present). Ignored when replacing an input, which carries the value itself.
theme'auto' | 'light' | 'dark''auto' 'auto' follows the page theme and re-resolves live; 'light'/'dark' pin the instance.
stylesbooleantrue false = headless: no stylesheet is injected; you style the .vsld-* markup yourself (see Theming).
onInputfunction | nullnull fn(value, slider) — called on every move. See Events.
onChangefunction | nullnull fn(value, slider) — called on release / commit. See Events.
labelsobject{ value: 'Value', min: 'Minimum value', max: 'Maximum value' } Thumb aria-label texts, merged key-by-key with the defaults. labels.value labels a single thumb; labels.min / labels.max label the low / high thumb of a dual slider.

Instance methods

SignatureReturnsDescription
getValue()number | [number, number] Current value — a number for single thumb, a fresh two-element array for dual.
setValue(value, opts?)this Sets the value, snapped to the step grid and clamped. A dual slider accepts [lo, hi] (auto-sorted) or a single number applied to both thumbs. When the value actually changes it fires onInput + onChange and both CustomEvents — pass { silent: true } to suppress all of them.
enable()this Re-enables interaction: thumbs get tabindex="0", aria-disabled is removed, form control(s) are re-enabled.
disable()this Disables interaction (cancels any active drag): thumbs get tabindex="-1" and aria-disabled="true"; the replaced/hidden input(s) get disabled so the slider doesn't submit, like a native control.
destroy()this Removes all built DOM, restores every attribute it touched (un-hides and re-enables a replaced input), removes its classes, and unregisters the instance.

Useful instance properties: el (root element), track, thumbs, values, input (the replaced input, or null in build mode), opts, dual.

Statics & helpers

StaticTypeDescription
Slider.defaultsobject The live defaults object (see Options). Mutate before constructing, e.g. Slider.defaults.styles = false for global headless mode.
Slider.create(target, options?)function → Slider Identical to new Slider(target, options).
Slider.get(target)function → Slider | null The instance bound to an element (selector or element; matches either the original target or the built root), or null.
Slider.autoInit(root?)function → Slider[] Initializes every [data-vsld] element under root (default document), skipping elements that already have an instance. A failing element logs an error and doesn't abort the rest. Returns the created instances.
Slider.saltstring | false CSS-isolation namespace token, default 'vc1'. Set your own token or false (no salting) before the first instance is created.
Slider.cssstring (getter) The full embedded stylesheet, rendered with the current salt — a starting point for headless styling.
Slider.versionstring '1.0.0'.
Slider.displayNamestring 'Slider' — family metadata used when registering with the VC core.
Slider.rootClassstring 'vsld' — the root element's class.
Slider.themeVarsobject Maps family theme keys to CSS custom properties: { accent: '--vsld-accent', radius: '--vsld-radius', font: '--vsld-font' }. Used by VC.config().
Slider.varScopesarray ['.vsld', '.vsld[data-theme=dark]'] — the (deliberately unsalted) scopes where theme variables are defined; VC.config() writes its overrides there.

Events & callbacks

onInput fires on every move (each drag tick, each key press, and a changed setValue); onChange fires on commit — pointer release when the value differs from where the drag started, every key press, and a changed setValue. Both receive (value, slider), where value is a number or [lo, hi] for dual.

CallbackArgumentsWhen
onInput(value, slider) Every value change: drag movement, each key press, non-silent setValue.
onChange(value, slider) Commit: pointer release with a changed value, each key press, non-silent setValue.

The root element also dispatches CustomEvents, so you can delegate without keeping a reference:

EventFires onBubblesCancelableevent.detail
slider:inputroot element (.vsld)yesno { value, slider }
slider:changeroot element (.vsld)yesno { value, slider }

In replace mode the hidden original input additionally receives real, bubbling (non-cancelable) native input and change events on the same occasions — dispatched before the matching callback and CustomEvent — so existing listeners and frameworks keep working.

Declarative init (data-* attributes)

Any element with a data-vsld attribute is initialized automatically on DOMContentLoaded (or immediately if the script loads later), and again by any explicit Slider.autoInit(root?) call — already-initialized elements are skipped.

<div data-vsld data-min="0" data-max="100" data-value="40" data-suffix="%"></div>
<div data-vsld data-value="20,80" data-name="range" data-tooltip="always"></div>
AttributeValue
data-vsldMarker only (no value) — opts the element into auto-init.
data-minNumber (empty ignored).
data-maxNumber (empty ignored).
data-stepNumber (empty ignored).
data-valueNumber, or "a,b" for a dual-thumb range.
data-tooltipdrag | always; false or 0 disables the bubble.
data-marksEmpty or true = per-step ticks; otherwise a JSON object of {"value": "label"} (invalid JSON falls back to true).
data-verticalBoolean — any value except false/0 is true.
data-disabledBoolean — same rule.
data-prefixString (non-empty).
data-suffixString (non-empty).
data-nameString (non-empty) — hidden form input name(s).
data-themeauto | light | dark (non-empty).
data-stylesBoolean — false/0 = headless.

format, labels, onInput and onChange have no declarative form — use the constructor, or listen for the slider:* CustomEvents.

Theming

With theme: 'auto' (the default) the theme is resolved as: VC core's shared engine when loaded, otherwise <html data-theme> / data-bs-theme → .dark / .light class on <html> → prefers-color-scheme — re-resolved live via a MutationObserver and a media-query listener. theme: 'light' / 'dark' pins an instance. The resolved theme is stamped on the root as data-theme.

All colors are CSS custom properties, defined on .vsld and re-defined on .vsld[data-theme=dark]:

PropertyLight defaultDark defaultUsed for
--vsld-accent#5b5bd6#7b7bea Fill bar, thumb ring, focus ring.
--vsld-bg#ffffff#1b1d24 Thumb body, tooltip text.
--vsld-text#1c1d21#e9eaf0 Component text, tooltip background.
--vsld-muted#72747e#989aa6 Mark dots and mark labels.
--vsld-faint#e7e7ec#31343f Track rail.
--vsld-shadow0 1px 4px rgba(24,25,32,.14), 0 1px 2px rgba(24,25,32,.08) 0 1px 4px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.35) Thumb shadow.
--vsld-radius8px8px Tooltip corner radius.
--vsld-fontsystem-ui stacksystem-ui stack Component font family.

CSS isolation: roots render as class="vsld vc1" and all structural rules ship salted (.vsld.vc1 .vsld-thumb { … }), so host-page design systems can't override the slider — while .vsld { --vsld-* } variable overrides keep working, because the variable definitions are deliberately unsalted. The injected stylesheet is inserted before the page's own CSS so your overrides win the cascade. Custom token: Slider.salt = 'acme' before the first instance; disable with Slider.salt = false. With the VC core loaded, VC.config({ accent: '#b45309' }) themes sliders and every other family component at once.

Headless: Slider.defaults.styles = false (or styles: false per instance) injects no CSS while keeping the full behavior — pointer capture, clamping, keyboard, ARIA, form sync. The stock stylesheet is available as the Slider.css string as a starting point. Markup contract:

.vsld.vsld-vertical.vsld-tip-always.vsld-disabled[data-theme=dark]
  .vsld-track                ← the rail (press/drag surface)
    .vsld-fill               ← range fill bar
    .vsld-mark               ← tick; one per step or per `marks` key
      .vsld-mark-label       ← only for object marks
    .vsld-thumb              ← role=slider; .vsld-active while dragging
      .vsld-tip              ← value bubble (absent when tooltip: false)
  input[type=hidden]         ← only when `name` given

Accessibility

Each thumb is a focusable role="slider" element carrying aria-valuemin / aria-valuemax / aria-valuenow, a formatted aria-valuetext, an aria-label from labels, and aria-orientation="vertical" when vertical. Dual thumbs advertise the sibling as their live limit (the low thumb's aria-valuemax is the high thumb's value, and vice versa). Disabled thumbs get aria-disabled="true" and tabindex="-1"; the tooltip bubble is aria-hidden. Transitions are dropped under prefers-reduced-motion: reduce.

KeyAction
ArrowLeft / ArrowDownDecrease by one step.
ArrowRight / ArrowUpIncrease by one step (ArrowUp increases even when vertical).
PageUpIncrease by 10 × step.
PageDownDecrease by 10 × step.
HomeJump to min (a dual slider's high thumb stops at the low thumb).
EndJump to max (a dual slider's low thumb stops at the high thumb).

Key presses are ignored while disabled or with Alt/Ctrl/Meta held. Every key press is both a move and a commit — it fires onInput and onChange.

Headless mode

styles: false injects nothing — you keep the full behavior (pointer capture, clamping, form sync), the ARIA slider semantics and keyboard handling, and the stable .vsld-* class hooks; you bring your own CSS. Each demo runs in its own <iframe>: the styled examples above already injected the slider stylesheet into this page, so only a clean document can show truly unstyled output.

Unstyled (styles: false)

The raw markup contract — track, fill and role="slider" thumb with class hooks, zero CSS injected. Tab to it and use the arrow keys: the behavior is all there.

new Slider('#s', {
  styles: false,        // zero CSS injected
  min: 0, max: 100, value: 40,
  onInput: v => out.value = v
})

Bring your own design (Tailwind)

The same headless dual slider mapped to a different design — gradient fill, ring on the active thumb, value tips and marks — loaded on demand from cdn.tailwindcss.com.

<style type="text/tailwindcss">
.vsld { @apply relative py-6; }
.vsld-track { @apply relative h-2 cursor-pointer
  rounded-full bg-slate-200; }
.vsld-fill { @apply absolute inset-y-0 rounded-full
  bg-gradient-to-r from-indigo-500 to-fuchsia-500; }
.vsld-thumb { @apply absolute top-1/2 h-5 w-5
  -translate-x-1/2 -translate-y-1/2 cursor-grab
  rounded-full border-2 border-indigo-500 bg-white
  shadow transition-transform; }
.vsld-thumb.vsld-active { @apply scale-125 cursor-grabbing
  ring-4 ring-indigo-500/25; }
.vsld-thumb:focus-visible { @apply outline-none ring-4
  ring-indigo-500/40; }
.vsld-tip { @apply pointer-events-none absolute bottom-7
  left-1/2 -translate-x-1/2 rounded-md bg-slate-900
  px-1.5 py-0.5 text-[11px] font-semibold text-white
  opacity-0 transition-opacity; }
.vsld-thumb.vsld-active .vsld-tip,
.vsld-thumb:focus-visible .vsld-tip { @apply opacity-100; }
.vsld-mark { @apply absolute top-1/2; }
.vsld-mark-label { @apply absolute left-1/2 top-3
  -translate-x-1/2 text-[11px] text-slate-400; }
</style>