Slider
Range input slider with a label, live numeric value display, accent-primary fill track, and customisable min/max/step.
<arc-slider>Overview
Slider is a range input for selecting a numeric value within a defined range. When a label is provided, the component renders a header row with the label on the left and the current numeric value on the right in monospace font, updating as the user drags the thumb. The track uses a gradient fill from accent-primary to the default border color to show the selected proportion.
The component wraps a native <input type="range"> element, so it keeps the browser's built-in accessibility: keyboard control (arrow keys for stepping) and screen reader announcement of the current value via aria-valuenow, aria-valuemin, and aria-valuemax. The step prop controls the increment granularity, which suits both coarse controls (volume 0-100) and fine-grained settings (opacity 0.00-1.00).
Slider fires arc-input on every movement for real-time UI updates and arc-change when the user releases the thumb, mirroring the native input/change event distinction. The thumb scales up and gains a blue glow on hover or focus, which signals that it is interactive.
Guidelines
When to use
- Provide a `label` so users can see both the purpose and the current value at a glance
- Choose a `step` that matches your data precision; use 1 for integers, 0.01 for percentages
- Use `arc-input` for real-time preview (e.g. adjusting a visual property) and `arc-change` for committing the final value
- Set meaningful `min` and `max` values that reflect the actual valid range for your use case
- Place Slider in a container wide enough for comfortable thumb dragging: at least 200px
When not to use
- Do not use Slider for exact numeric entry where the user needs to type a specific number; use Input with `type="number"` instead
- Do not set a `step` so small that the slider has thousands of positions: it becomes imprecise with mouse input
- Do not omit `label` when the slider is standalone: without context the value readout is meaningless
- Do not use Slider for binary on/off choices; use Toggle instead
- Avoid placing multiple sliders in a narrow column without sufficient vertical spacing between them
Features
- Visual fill track using a CSS gradient from accent-primary to the border color, proportional to the current value
- Header row displaying the label and current numeric value in monospace font when `label` is set
- Configurable `min`, `max`, and `step` props for precise range and increment control
- Thumb hover and focus effects with scale-up and accent-primary glow shadow
- Native keyboard support via arrow keys, Page Up/Down, and Home/End from the underlying range input
- Dual events: `arc-input` fires continuously during drag, `arc-change` fires on release
- ARIA value attributes: `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, and `aria-label`
- Disabled state at 40% opacity with pointer events blocked
Preview
Usage
Layout and styling work without JavaScript via the HTML/CSS versions. Interactive features like events and state management require the Web Component or a framework wrapper.
<script type="module" src="@arclux/arc-ui"></script>
<!-- Basic labeled slider -->
<arc-slider label="Volume" value="50" min="0" max="100"></arc-slider>
<!-- Fine-grained step for opacity -->
<arc-slider label="Opacity" value="0.8" min="0" max="1" step="0.01"></arc-slider>
<!-- Temperature range -->
<arc-slider label="Color Temperature" value="4500" min="2700" max="6500" step="100"></arc-slider>
<!-- Disabled state -->
<arc-slider label="Locked" value="30" disabled></arc-slider>
<script>
const slider = document.querySelector('arc-slider');
// Real-time preview while dragging
slider.addEventListener('arc-input', (e) => {
document.body.style.opacity = e.detail.value / 100;
});
// Commit final value on release
slider.addEventListener('arc-change', (e) => {
console.log('Final value:', e.detail.value);
});
</script>import { Slider } from '@arclux/arc-ui-react';
import { useState } from 'react';
function VolumeControl() {
const [volume, setVolume] = useState(50);
return (
<Slider
label="Volume"
value={volume}
min={0}
max={100}
onArcInput={(e) => setVolume(e.detail.value)}
onArcChange={(e) => saveVolume(e.detail.value)}
/>
);
}
function ImageEditor() {
const [opacity, setOpacity] = useState(1);
const [blur, setBlur] = useState(0);
const [brightness, setBrightness] = useState(100);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 400 }}>
<Slider label="Opacity" value={opacity} min={0} max={1} step={0.01}
onArcInput={(e) => setOpacity(e.detail.value)} />
<Slider label="Blur" value={blur} min={0} max={20} step={0.5}
onArcInput={(e) => setBlur(e.detail.value)} />
<Slider label="Brightness" value={brightness} min={0} max={200}
onArcInput={(e) => setBrightness(e.detail.value)} />
</div>
);
}<script setup>
import { Slider } from '@arclux/arc-ui-vue';
import { ref } from 'vue';
const fontSize = ref(16);
const lineHeight = ref(1.5);
const letterSpacing = ref(0);
</script>
<template>
<div style="display:flex; flex-direction:column; gap:16px; max-width:400px;">
<Slider label="Font Size" :value="fontSize" :min="10" :max="48"
@arc-input="fontSize = $event.detail.value" />
<Slider label="Line Height" :value="lineHeight" :min="1" :max="3" :step="0.1"
@arc-input="lineHeight = $event.detail.value" />
<Slider label="Letter Spacing" :value="letterSpacing" :min="-2" :max="10" :step="0.5"
@arc-input="letterSpacing = $event.detail.value" />
</div>
<p :style="{ fontSize: fontSize + 'px', lineHeight, letterSpacing: letterSpacing + 'px' }">
Preview text with live adjustments.
</p>
</template><script>
import { Slider } from '@arclux/arc-ui-svelte';
let hue = 220;
let saturation = 70;
let lightness = 50;
$: color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
</script>
<div style="display:flex; flex-direction:column; gap:16px; max-width:400px;">
<Slider label="Hue" value={hue} min={0} max={360}
on:arc-input={(e) => hue = e.detail.value} />
<Slider label="Saturation" value={saturation} min={0} max={100}
on:arc-input={(e) => saturation = e.detail.value} />
<Slider label="Lightness" value={lightness} min={0} max={100}
on:arc-input={(e) => lightness = e.detail.value} />
</div>
<div style="width:80px; height:80px; border-radius:12px; margin-top:16px; background:{color}"></div>import { Component } from '@angular/core';
import { Slider } from '@arclux/arc-ui-angular';
@Component({
imports: [Slider],
template: `
<div style="display:flex; flex-direction:column; gap:16px; max-width:400px;">
<arc-slider label="Playback Speed" [value]="speed" [min]="0.25" [max]="3" [step]="0.25"
(arc-input)="speed = $event.detail.value"></arc-slider>
<arc-slider label="Seek" [value]="position" [min]="0" [max]="duration"
(arc-input)="onSeek($event.detail.value)"
(arc-change)="onSeekCommit($event.detail.value)"></arc-slider>
<arc-slider label="Volume" [value]="volume" [min]="0" [max]="100"
(arc-change)="volume = $event.detail.value"></arc-slider>
</div>
`,
})
export class MediaPlayerComponent {
speed = 1;
position = 0;
duration = 240;
volume = 75;
onSeek(val: number) { /* live preview seek position */ }
onSeekCommit(val: number) { /* commit seek to player */ }
}import { Slider } from '@arclux/arc-ui-solid';
import { createSignal } from 'solid-js';
function PricingEstimator() {
const [seats, setSeats] = createSignal(5);
const pricePerSeat = 12;
return (
<div style={{ 'max-width': '400px' }}>
<Slider
label="Team Size"
value={seats()}
min={1}
max={100}
onArcInput={(e) => setSeats(e.detail.value)}
/>
<p style={{ 'margin-top': '12px', 'font-size': '14px', color: 'var(--text-muted)' }}>
{seats()} seats × ${pricePerSeat}/mo = <strong>${seats() * pricePerSeat}/mo</strong>
</p>
</div>
);
}import { Slider } from '@arclux/arc-ui-preact';
import { useState } from 'preact/hooks';
function BorderRadiusTool() {
const [radius, setRadius] = useState(8);
const [padding, setPadding] = useState(16);
return (
<div style={{ maxWidth: 400 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Slider label="Border Radius" value={radius} min={0} max={50}
onArcInput={(e) => setRadius(e.detail.value)} />
<Slider label="Padding" value={padding} min={0} max={64}
onArcInput={(e) => setPadding(e.detail.value)} />
</div>
<div style={{
marginTop: 24,
borderRadius: radius,
padding,
background: 'var(--bg-elevated)',
border: '1px solid var(--border-default)',
}}>
Preview box
</div>
</div>
);
}<!-- arc-slider is interactive — requires JS -->
<arc-slider></arc-slider><!-- arc-slider is interactive — requires JS -->
<arc-slider></arc-slider>API
valuenumber0- Current slider value. Reflected as an attribute and updated on user interaction.
minnumber0- Minimum allowed value at the left edge of the track.
maxnumber100- Maximum allowed value at the right edge of the track.
stepnumber1- Increment granularity. The value snaps to multiples of this number.
namestring''disabledbooleanfalse- Disables interaction, reducing opacity to 40% and blocking pointer events.
labelstring''- Label text displayed above the slider with the current value shown on the right.
readonlybooleanfalse- Prevents dragging and arrow-key changes while the thumb stays focusable and the value still submits.
size'sm' | 'md' | 'lg''md'- Control size.
mdis the default;smandlgscale the track and thumb. formAssociatedbooleantruepropertiesobject{ // flag(), unlike `disabled`. The exclusion in props.js is specifically // about form-associated *platform* semantics: a `disabled` content // attribute that is merely present makes the element actually disabled // per the HTML spec, and formDisabledCallback assigns the property back, // so no converter can win. Neither of these is platform-mapped: // `required` is enforced by _computeValidity() below and `readonly` by // each component's own interaction handlers, so the stock converter buys // nothing here and costs the usual bug: `required="false"` read as true, // blocking submission of a form the author meant to leave optional. // Finding #48's shape, across all 26 form controls at once. required: flag(false), readonly: flag(false), }- Lit merges static properties up the prototype chain, so every consumer
gets these without declaring them.
requiredparticipates in constraint validation below;readonlyreflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state). autoValidatesbooleantrue- Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
formvalidityvalidationMessagerequiredbooleanfalse
Methods
checkValidity()boolean- Whether the control currently satisfies its constraints, per the native
constraint-validation API. Fires
invalidon the element when it does not, and reports nothing to the user. reportValidity()boolean- As checkValidity(), but also shows the browser's validation message against the control when it fails.
Events
arc-inputdetail:{ value: number }- Fired continuously as the user drags the thumb. Use for real-time preview updates like adjusting opacity, volume, or a CSS property live.
arc-changedetail:{ value: number }- Fired once when the user releases the thumb, indicating the final committed value. Use for persisting the value to a database or triggering an expensive operation.
See Also
- Number InputA numeric stepper input with decrement and increment buttons flanking a central text field, supporting min/max clamping, step increments, and keyboard shortcuts.
- InputVersatile form control supporting single-line text, email, password, and multiline textarea modes with built-in label, placeholder, and validation states. Pairs with Form for complete data-entry workflows.
- RatingA star-based rating input with hover preview, keyboard navigation, filled/unfilled SVG stars, and configurable max value.