Slider
A slider input for selecting a single value or a range of values within a minimum and maximum.
Playground
<Slider
label="Volume"
v-model="value"
/><script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const value = ref([25])
</script>
<template>
<div class="w-full max-w-md">
<Slider v-model="value" />
</div>
</template>Sizes
<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const small = ref([30])
const medium = ref([60])
</script>
<template>
<div class="flex flex-col gap-6 w-full max-w-md">
<Slider v-model="small" size="sm" label="Small" />
<Slider v-model="medium" size="md" label="Medium" />
</div>
</template>Range
Use a two-element modelValue to render two thumbs.
<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>Negative Values
When min is negative the slider fills bidirectionally from the zero-crossing, so positive and negative values are visually distinct.
<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const value = ref([25])
</script>
<template>
<div class="w-full max-w-md">
<Slider v-model="value" :min="-50" :max="100" />
</div>
</template>Labeling
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Checkbox, Slider } from 'frappe-ui'
const value = ref([40])
const required = ref(true)
const showError = ref(false)
const error = computed(() =>
showError.value ? 'Please pick a value greater than 50.' : '',
)
</script>
<template>
<div class="flex gap-8 items-start">
<Slider
v-model="value"
label="Volume"
description="Adjust the volume."
:error="error"
:required="required"
class="w-96"
/>
<div
class="flex flex-col gap-2 items-start border-l border-outline-gray-2 pl-6 shrink-0"
>
<Checkbox v-model="required" label="required" />
<Checkbox v-model="showError" label="show error" />
</div>
</div>
</template>The accessible name lands on the thumb, which is the element carrying role="slider". A caller's aria-labelledby or aria-label overrides the name derived from label, so the visible label and the announced name can differ. Use one or the other.
required is the one exception to that routing. The ARIA role table does not list aria-required for slider, so setting it fails the aria-allowed-attr audit rule and screen readers ignore it. The state is announced from inside the label instead, as sr-only text next to the asterisk. The asterisk itself is aria-hidden, and data-required is a styling hook.
That has a consequence: required is announced only while the name comes from the rendered label. A caller aria-label or aria-labelledby replaces it as the name, so the thumb then announces the name without "(required)". A #label slot replaces the whole label body, asterisk included, so nothing renders it at all — the slot receives { required } for that. In each of the three cases, render or word the required state yourself.
A range renders one thumb per value. When a name is set, each thumb is qualified with it ("Price minimum", "Price maximum", or "Stops value 2 of 3" past two thumbs) so the endpoints are told apart. An unnamed range keeps the plain "Minimum"/"Maximum" names.
Any other aria-* you set is copied to every thumb, so a range gets one shared value for all of them. aria-valuetext on a range is announced identically at both endpoints. aria-hidden is the exception and stays on the root, because it hides a subtree rather than describing the control. It also makes the control inert: the thumbs stay in the tab order otherwise, which puts keyboard focus inside a subtree a screen reader cannot see.
value-commit
value-commit fires once when the user finishes dragging the slider — useful for triggering side effects only at drag end, not on every step.
<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const value = ref([50])
const commits = ref<number[][]>([])
function onValueCommit(committed: number[]) {
commits.value = [...commits.value, committed]
}
</script>
<!--
`value-commit` fires once at the end of a drag, not on every step.
Drag the slider thumb and release to log committed values below.
-->
<template>
<div class="flex flex-col gap-3 w-full max-w-md">
<Slider
v-model="value"
label="Drag and release to commit"
@value-commit="onValueCommit"
/>
<p class="text-p-sm text-ink-gray-6">
Commits: {{ commits.length ? commits.map((c) => c.join(',')).join(' · ') : '—' }}
</p>
</div>
</template>States
<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from 'frappe-ui'
const a = ref([30])
const b = ref([30])
const c = ref([30])
const d = ref([30])
</script>
<template>
<div class="flex flex-col gap-6 w-full max-w-md">
<Slider v-model="a" label="Default" />
<Slider v-model="b" label="Required" required />
<Slider v-model="c" label="Disabled" disabled />
<Slider v-model="d" label="With error" error="Required." />
</div>
</template>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"`. May be either a string or an `Error` object whose `messages?: string[]` is rendered as stacked lines (with `Error.message` as the fallback).
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.