Slider
Customizable slider component.
Source Style- React
- Svelte
- Vue
function App() { const [value, setValue] = useState(64) return ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 8, }} > <Slider.Root className={sliderTheme.root} value={value} min={0} max={100} onChange={(v) => setValue(v)} > <Slider.Track className={sliderTheme.track}> <Slider.Thumb className={sliderTheme.thumb} aria-label="Level" /> </Slider.Track> </Slider.Root> <span style={{ width: '2rem', }} > {value} </span> </div> ) }
/** The demo theme for `Slider`, as a CSS Module: one class per part, applied by* the caller. Copy this file into your project and edit it — it is a starting* point, not a dependency.** It says what the parts look like, not where they go: the component places* them itself.*//** The size the thumb is drawn at, and the room the slider leaves for it. Both* parts need the value and nothing inherits it, so it is composed into each* of them and stays written once.*/.thumbSize {--thumb-size: 22px;}.root {composes: thumbSize;display: inline-flex;flex-direction: column;margin: calc(var(--thumb-size) / 2);cursor: pointer;outline: 0;-webkit-tap-highlight-color: transparent;&[data-orientation='vertical'] {flex-direction: row;}&[data-readonly] {cursor: not-allowed;}}.thumb {composes: thumbSize;--color: oklch(59.5% 0.17548 266.373);width: var(--thumb-size);height: var(--thumb-size);background: var(--color);border-radius: 50%;outline: none;&[data-disabled] {background: oklch(50.462% 0.03307 270.021);}&:focus-within {box-shadow: 0px 0px 0px 3pxcolor-mix(in srgb, var(--color) 20%, transparent);}}.track {--active-color: oklch(69.212% 0.12903 267.633);--inactive-color: oklch(95% 0 0);--length: 140px;--thickness: 10px;/* Which way the fill runs. `--percent` is where the thumb sits on screen,so a flipped slider only swaps which side of it is the filled one. */--axis: to right;width: var(--length);height: var(--thickness);border-radius: calc(var(--thickness) / 2);/* The component sets `--percent` and paints nothing itself, so the fill isyours to change: a second colour stop, a gradient, an image. */background: linear-gradient(var(--axis),var(--active-color) var(--percent),var(--inactive-color) var(--percent));&[data-flipped] {background: linear-gradient(var(--axis),var(--inactive-color) var(--percent),var(--active-color) var(--percent));}&[data-orientation='horizontal'] {min-width: 50px;}&[data-orientation='vertical'] {--axis: to bottom;width: var(--thickness);height: var(--length);min-height: 50px;}&:hover {--active-color: oklch(64.276% 0.15244 266.932);--inactive-color: #e0e0e0;}&[data-disabled] {--active-color: oklch(75.327% 0.01237 264.547);--inactive-color: oklch(89.755% 0.0001 271.152);}}.marks {/* Space between the marks and the track. */--gap: 6px;display: block;margin-top: var(--gap);&[data-orientation='vertical'] {margin-top: 0;margin-left: var(--gap);}}.marksOption {/* Read by the mark and the label below, which is why they are set here. */--thickness: 1px;--length: 0.5rem;--gap: 2px;display: flex;color: oklch(25.197% 0.00003 271.152);flex-direction: column;justify-content: center;align-items: center;&[data-orientation='vertical'] {flex-direction: row;}}.mark {background-color: currentColor;width: var(--thickness);height: var(--length);margin-bottom: var(--gap);&[data-orientation='vertical'] {width: var(--length);height: var(--thickness);margin-bottom: 0;margin-right: var(--gap);}}.label {text-align: right;}/** Dark mode follows whatever marks the page — `.dark` and `[data-theme=dark]`* are what this site and Storybook set. Swap them for however your own app* says it, or read `prefers-color-scheme` instead.*/:global(.dark) .thumb,:global([data-theme='dark']) .thumb {--color: oklch(64.5% 0.17548 266.373);&:hover {--color: oklch(67.5% 0.17548 266.373);}}:global(.dark) .track,:global([data-theme='dark']) .track {--active-color: oklch(59.5% 0.17548 266.373);--inactive-color: oklch(41.7% 0 0);&:hover {--active-color: oklch(62.5% 0.17548 266.373);--inactive-color: oklch(44.7% 0 0);}&[data-disabled] {--active-color: oklch(75.327% 0.01237 264.547);--inactive-color: oklch(89.755% 0.0001 271.152);}}
<script lang="ts">
import { Slider } from '@tremolo-ui/svelte'
// Copy this file from the Styling page into your own project.
import sliderTheme from './Slider.module.css'
let value = $state(64)
</script>
<Slider.Root class={sliderTheme.root} bind:value min={0} max={100}>
<Slider.Track class={sliderTheme.track}>
<Slider.Thumb class={sliderTheme.thumb} aria-label="Level" />
</Slider.Track>
</Slider.Root>
<span>{value}</span>
<script setup lang="ts">
import { ref } from 'vue'
import { Slider, SliderThumb, SliderTrack } from '@tremolo-ui/vue'
// Copy this file from the Styling page into your own project.
import sliderTheme from './Slider.module.css'
const value = ref(64)
</script>
<template>
<Slider v-model="value" :class="sliderTheme.root" :min="0" :max="100">
<SliderTrack :class="sliderTheme.track">
<SliderThumb :class="sliderTheme.thumb" aria-label="Level" />
</SliderTrack>
</Slider>
<span>{{ value }}</span>
</template>
Import
- React
- Svelte
- Vue
import { Slider } from '@tremolo-ui/react'
import { Slider } from '@tremolo-ui/svelte'
import {
Slider,
SliderMarks,
SliderMarksOption,
SliderThumb,
SliderTrack,
} from '@tremolo-ui/vue'
API Reference
Each part lists the props of its own. It also takes the attributes of the
element it renders, such as className and style.
In Svelte and Vue the props are the same, written their way — class rather
than className, bind:value or v-model, snippets or slots for content. See
Svelte and Vue.
State is on the element as a data attribute, so it is a selector away — see Styling. An attribute is only there while its state is on.
Root
Holds the value and the settings, and passes them to the parts inside.
| Prop | Type | Default |
|---|---|---|
value* | number | — |
min* | number | — |
max* | number | — |
step | number | 1 |
scale | Scale | linearScale |
orientation | "horizontal" | "vertical" | 'horizontal' |
reverse | boolean | false |
dragCursor | Cursor | 'pointer' |
wheel | ModifierValue<InputEventOption> | null | ['raw', 1] |
dragSensitivity | ModifierValue<number> | { default: 1, shift: 0.1 } |
keyboard | ModifierValue<InputEventOption> | null | { default: ['raw', 1], shift: ['raw', 0.1] } |
disabled | boolean | — |
readOnly | boolean | — |
onChange | (value: number) => void | — |
onChangeStart | (value: number, source: ChangeSource) => void | — |
onChangeEnd | (value: number, source: ChangeSource) => void | — |
changeEndDelay | number | 500 |
children* | ReactNode | — |
actionsRef | Ref<SliderMethods> | — |
Full reference: SliderProps
| Data attribute | Values |
|---|---|
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled is set |
[data-readonly] | Present when readonly is set |
Track
The bar the value runs along. It sets --percent to where the value is, so
the theme can fill the part below it. Slider.Thumb goes inside it.
| Prop | Type | Default |
|---|---|---|
length | string | number | — |
thickness | string | number | — |
activeColor | string | — |
inactiveColor | string | — |
Full reference: SliderTrackProps
| Custom property | Written by | What it sets |
|---|---|---|
--length | length | How long the track is along the axis the slider runs. |
--thickness | thickness | How thick the track is across that axis. |
--active-color | activeColor | Colour of the part from min to the value. |
--inactive-color | inactiveColor | Colour of the rest of the track. |
--percent | the component | Where the value sits, as a percentage. |
--percent is the one number CSS cannot work out on its own, and it is why the
track draws nothing of its own: the fill is a rule in the theme rather than
something the package paints for you.
.track {
background: linear-gradient(
var(--axis),
var(--active-color) var(--percent),
var(--inactive-color) var(--percent)
);
}
Replace that rule and the track can be anything — a second colour stop, an image, a dotted scale that fills in.
| Data attribute | Values |
|---|---|
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled is set |
[data-flipped] | Present when the value grows from the far end: vertical or reverse, but not both |
Thumb
The handle. A visually hidden range input inside it takes the focus and carries the ARIA.
Its children are drawn inside it. The thumb is one element either way, so what
is passed there is decoration on top of it rather than a replacement for it —
className and style are how its own appearance is changed.
| Prop | Type | Default |
|---|---|---|
color | string | — |
actionsRef | Ref<SliderThumbMethods> | — |
Full reference: SliderThumbProps
| Custom property | Written by | What it sets |
|---|---|---|
--color | color | Colour of the thumb. |
| Data attribute | Values |
|---|---|
[data-disabled] | Present when disabled is set |
[data-readonly] | Present when readonly is set |
Marks
A row of marks along the track, built from options or from
Slider.MarksOption children, one per mark. The children are ignored while
options is set.
| Prop | Type | Default |
|---|---|---|
gap | string | number | — |
options | MarksOptions | — |
Full reference: SliderMarksProps
| Custom property | Written by | What it sets |
|---|---|---|
--gap | gap | Space between the marks and the track. |
| Data attribute | Values |
|---|---|
[data-orientation] | "horizontal" | "vertical" |
MarksOption
One mark and its label, placed at value.
| Prop | Type | Default |
|---|---|---|
value* | number | — |
mark | boolean | true |
label | string | number | null | — |
thickness | string | number | — |
length | string | number | — |
gap | string | number | — |
classes | { mark?: string; label?: string } | — |
styles | { mark?: CSSProperties; label?: CSSProperties } | — |
Full reference: SliderMarksOptionProps
| Custom property | Written by | What it sets |
|---|---|---|
--thickness | thickness | Thickness of the mark. |
--length | length | Length of the mark. |
--gap | gap | Space between the mark and the label. |
Set the label's width through styles.label or its class in classes.label.
| Data attribute | Values |
|---|---|
[data-orientation] | "horizontal" | "vertical" |
The mark inside the option carries [data-orientation] too.