Frappe UIFrappe UI

Slider

A slider input for selecting a single value or a range of values within a minimum and maximum.

Playground

label
min
max
step
size
range
disabled
<Slider
  label="Volume"
  v-model="value"
/>
vue
<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

vue
<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.

minimummaximum
20 – 80
vue
<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.

vue
<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

Adjust the volume.

vue
<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.

Commits: —

vue
<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

vue
<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
typescript
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
= 1
number

Step interval between slider values.

max
= 100
number

Maximum allowed slider value.

min
= 0
number

Minimum allowed slider value. Negative values enable bidirectional fill from zero.

size
= "sm"
RangeSize

Visual size of the slider.

disabled
= false
boolean

Disables the slider.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

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
string | FrappeUIError

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).

required
boolean

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.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

modelValue
SliderValue

The current slider value (controlled).

label
{ required: boolean; }

Overrides the rendered label content. Receives `{ required }`.

description

Overrides the rendered description content.

update:modelValue
[value: SliderValue | undefined]

Fired when the model value changes.

value-commit
[value: SliderValue]

Fired once when the user finishes dragging the slider.