Slider
A track with one or more handles for picking a number, or a range of numbers, between a minimum and a maximum.
<Slider
label="Volume"
v-model="value"
/>Examples
Price range filter
A v-model with two numbers draws two handles, one for each end of the range.
<script setup lang="ts">
import { ref } from 'vue'
import { Badge, Slider } from 'frappe-ui'
const value = ref([20, 80])
</script>
<template>
<div class="flex flex-col gap-3 w-full max-w-md">
<Slider v-model="value" label="Price range" />
<Badge class="w-fit !rounded-1">{{ value.join(' – ') }}</Badge>
</div>
</template>Exposure adjustment
With a negative min, the filled part of the track starts at zero, so values below and above zero look different.
<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const exposure = ref([-20])
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-2">
<Slider v-model="exposure" label="Exposure" :min="-100" :max="100" />
<span class="text-sm text-ink-gray-5 tabular-nums">
{{ exposure[0] > 0 ? '+' : '' }}{{ exposure[0] }}
</span>
</div>
</template>Save when the handle is released
value-commit fires once when a drag ends. Save there instead of on every change of v-model.
<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const brightness = ref([60])
const saved = ref<number | null>(null)
const saves = ref(0)
// Runs once when the drag ends, not on every step.
function save([value]: number[]) {
saved.value = value
saves.value++
}
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-2">
<Slider
v-model="brightness"
label="Screen brightness"
@value-commit="save"
/>
<p class="text-p-sm text-ink-gray-5">
Showing {{ brightness[0] }}.
<template v-if="saved !== null">
Saved {{ saved }} ({{ saves }} {{ saves === 1 ? 'save' : 'saves' }}).
</template>
<template v-else>Not saved yet.</template>
</p>
</div>
</template>Behavior
Value
The value is always an array of numbers: [25] for one handle, [20, 80] for a range. Each number draws a handle. An empty or missing value shows one handle at min, without updating the model.
min defaults to 0, max to 100 and step to 1.
Negative values
When min is below zero and max is above it, the filled part of the track runs from zero to the handle, in either direction. With a range, it runs between the two handles as usual.
Committed values
update:modelValue fires on every step of a drag. value-commit fires with the final value once the drag ends. Each key press also changes the value in one step, so it fires both events.
Width
The slider fills the width of its container. A width class such as w-64 replaces that width.
Label, description and error
label renders above the track and description below it. error renders below the track and hides description. It takes a string, an array of strings (one line each), or an Error, the same values as ErrorMessage. An empty string or an empty array means no error. required adds a red asterisk to the label.
The #label slot replaces the label text and the required marker, and receives { required }. A #description slot is not hidden by error. It renders above the error.
Attributes
class and style go on the outer element: the wrapper when a label, description or error shows, and the slider otherwise. aria-* attributes go on every handle. Every other attribute and listener goes on the slider's root element.
Accessibility
Each handle has role="slider" and is in the tab order.
| Keys | Action |
|---|---|
ArrowLeft ArrowDown / ArrowRight ArrowUp | Decrease / increase by step |
Shift + arrow, PageDown / PageUp | Decrease / increase by 10 steps |
Home / End | Set the first handle to min / the last handle to max |
Names
The name from label lands on each handle. A caller's aria-labelledby or aria-label replaces it, so the visible label and the announced name can differ. Use one or the other.
In a range, each handle adds its part to the name: "Price minimum" and "Price maximum", or "Stops value 2 of 3" with more than two handles. A range with no name uses "Minimum" and "Maximum".
Required
The slider role does not allow aria-required, so required is announced from the label instead, as hidden "(required)" text next to the asterisk. The asterisk itself is hidden from screen readers.
This works only while the name comes from the rendered label. In these cases you have to show or word the required state yourself:
- A caller
aria-labeloraria-labelledbyreplaces the label as the name, so the handle is announced without "(required)". - A
#labelslot replaces the whole label, asterisk included. The slot receives{ required }for this.
Other ARIA attributes
Any other aria-* you set is copied to every handle, so a range gets one value for all of them. aria-valuetext on a range is announced the same at both ends.
aria-hidden stays on the root instead, because it hides a whole subtree. It also makes the slider inert. Without that, the handles would stay in the tab order while screen readers could not see them.
API Reference
Show types
import type { RangeSize } from '../../composables/inputTypes'
import type { InputLabelingProps } from '../../composables/useInputLabeling'
/**
* Value model for the Slider component.
* Use one number for a single-value slider or two numbers for a range slider.
*
* @example
* const single: SliderValue = [25]
*
* @example
* const range: SliderValue = [20, 80]
*/
export type SliderValue = number[]
export interface SliderProps extends InputLabelingProps {
/** Step interval between slider values. */
step?: number
/** Maximum allowed slider value. */
max?: number
/** Minimum allowed slider value. Negative values enable bidirectional fill from zero. */
min?: number
/** Visual size of the slider. */
size?: RangeSize
/** Disables the slider. */
disabled?: boolean
}
export interface SliderEmits {
/** Fired once when the user finishes dragging the slider. */
'value-commit': [value: SliderValue]
}Step interval between slider values.
Maximum allowed slider value.
Minimum allowed slider value. Negative values enable bidirectional fill from zero.
Visual size of the slider.
Disables the slider.
Label rendered above (or beside, for binary controls) the input.
Helper text rendered below the input. Hidden when `error` is set. A `#description` slot is not: it renders beside the error, and is referenced alongside it.
Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. Takes a string, an array of strings, or an `Error` whose `messages` are rendered as stacked lines (with `Error.message` as the fallback). This is the same value `ErrorMessage.message` takes. An empty array and an empty string both mean no error.
Marks the field as required. Renders an asterisk next to the label, with `sr-only` text that announces it, and forwards `required` / `aria-required` to the underlying control where the control's role allows it. `data-required` is set either way.
HTML id of the underlying control. Auto-generated via `useId()` if omitted.
The current slider value (controlled).
| Slot | Payload |
|---|---|
label | { required: boolean; } Overrides the rendered label content. Receives `{ required }`. |
description | — Overrides the rendered description content. |
Overrides the rendered label content. Receives `{ required }`.
Overrides the rendered description content.
| Event | Payload |
|---|---|
update:modelValue | [value: SliderValue | undefined] Fired when the model value changes. |
value-commit | [value: SliderValue] Fired once when the user finishes dragging the slider. |
Fired when the model value changes.
Fired once when the user finishes dragging the slider.
| Template ref | Type |
|---|---|
focus | (options?: FocusOptions) => void Moves focus to the thumb. A range slider focuses its first thumb. |
Moves focus to the thumb. A range slider focuses its first thumb.