Slider

Picks a number within a range by dragging.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider } from '@hina-ui/vue'

  const volume = ref(60)
</script>

<template>
  <Slider v-model="volume" aria-label="Volume" class="w-64" />
</template>
vue

Usage

import { Slider } from '@hina-ui/vue'
ts

The slider picks a number between min and max, bound with v-model. The value updates continuously while dragging, and commit fires once on release. Attributes it does not declare land on the thumb, so name it with aria-label or aria-labelledby. Width belongs to the layout; the slider fills its container.

Value: 40

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider, Stack, Text } from '@hina-ui/vue'

  const brightness = ref(40)
</script>

<template>
  <Stack gap="sm" class="w-64">
    <Slider v-model="brightness" aria-label="Brightness" />
    <Text size="sm" tone="muted">Value: {{ brightness }}</Text>
  </Stack>
</template>
vue

Examples

Range and step

min, max and step set the range and granularity; the arrow keys move by step too.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider } from '@hina-ui/vue'

  const rating = ref(3.5)
</script>

<template>
  <Slider v-model="rating" :min="1" :max="5" :step="0.5" aria-label="Rating" class="w-64" />
</template>
vue

Direction

dir accepts ltr or rtl and takes precedence over Reka’s global direction configuration. Without either setting, it inherits dir from the surrounding elements. Direction changes update the track, thumbs, marks and labels together.

In RTL, the minimum is on the right and the maximum on the left. ArrowRight decreases the value, ArrowLeft increases it, and Home / End still move to the minimum / maximum.

LTR

RTL

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider, Stack, Text } from '@hina-ui/vue'

  const value = ref(25)
  const marks = [0, 25, 50, 75, 100].map(value => ({ value, label: String(value) }))
</script>

<template>
  <Stack gap="lg" class="w-72 max-w-full">
    <Stack v-for="dir in ['ltr', 'rtl'] as const" :key="dir" gap="xs">
      <Text size="sm" tone="muted">{{ dir.toUpperCase() }}</Text>
      <Slider
        v-model="value"
        :dir="dir"
        :marks="marks"
        :step="5"
        :aria-label="'Value (' + dir.toUpperCase() + ')'"
      />
    </Stack>
  </Stack>
</template>
vue

Marks

marks places ticks on the track; a mark with a label shows its text underneath.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider } from '@hina-ui/vue'

  const speed = ref(50)
  const marks = [
    { value: 0, label: 'Slow' },
    { value: 25 },
    { value: 50, label: 'Normal' },
    { value: 75 },
    { value: 100, label: 'Fast' },
  ]
</script>

<template>
  <Slider v-model="speed" :step="25" :marks="marks" aria-label="Page turn speed" class="w-64" />
</template>
vue

Value label

The value label is a Tooltip with the thumb as its trigger: it follows the thumb and flips to the other side near the viewport edge. By default the current value shows on hover, focus and while dragging; label="always" keeps it visible and label="none" removes it. format customises the text, which defaults to the number formatted for the current language. Like the IconButton hint it needs a TooltipProvider in the app, and stays hidden without one.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider, Stack } from '@hina-ui/vue'

  const quality = ref(80)
  const opacity = ref(30)
</script>

<template>
  <Stack gap="lg" class="w-64 pt-6">
    <Slider
      v-model="quality"
      label="always"
      :format="(v: number) => `${v}%`"
      aria-label="Image quality"
    />
    <Slider v-model="opacity" label="none" aria-label="Overlay opacity" />
  </Stack>
</template>
vue

Sizes

size is sm, md or lg; the track is 20, 24 or 28 pixels tall and the thumb 12, 16 or 20, the same as Switch.

<script setup lang="ts">
  import { Slider, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack gap="sm" class="w-64">
    <Slider size="sm" :model-value="30" aria-label="Small" />
    <Slider size="md" :model-value="50" aria-label="Medium" />
    <Slider size="lg" :model-value="70" aria-label="Large" />
  </Stack>
</template>
vue

States

disabled disables the whole slider.

<script setup lang="ts">
  import { Slider } from '@hina-ui/vue'
</script>

<template>
  <Slider disabled :model-value="45" aria-label="Disabled" class="w-64" />
</template>
vue

Commit on release

commit fires only when a drag or a key press ends, which suits expensive work such as sending a request.

While dragging: 20, on release: 20

<script setup lang="ts">
  import { ref } from 'vue'
  import { Slider, Stack, Text } from '@hina-ui/vue'

  const value = ref(20)
  const committed = ref(20)
</script>

<template>
  <Stack gap="sm" class="w-64">
    <Slider v-model="value" aria-label="Threshold" @commit="committed = $event" />
    <Text size="sm" tone="muted">While dragging: {{ value }}, on release: {{ committed }}</Text>
  </Stack>
</template>
vue

In a form

Inside a FormField the label is linked to the thumb through aria-labelledby, and the description and error message are rendered by the field; validation rules and submission belong to the Form.

Below 60 the image degrades visibly

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Form, FormField, Slider, Text } from '@hina-ui/vue'

  const schema = v.object({
    quality: v.pipe(v.number(), v.minValue(60, 'Quality must be at least 60')),
  })

  const values = reactive({ quality: 40 })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = JSON.stringify(data)
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField
      name="quality"
      label="Export quality"
      description="Below 60 the image degrades visibly"
    >
      <Slider v-model="values.quality" :min="0" :max="100" :step="5" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">Export</Button>
    <Text v-if="saved" tone="muted" size="sm">Exported: {{ saved }}</Text>
  </Form>
</template>
vue

Behaviour

  • Clicking anywhere on the track jumps the thumb there and starts dragging; while dragging the thumb and the fill follow the pointer without a transition, and jumps from clicks or keys are eased.
  • Tab lands on the thumb; the left and right arrows move by step, PageUp / PageDown move by a larger step, Home / End jump to the ends.
  • Hovering anywhere on the slider inks the thumb and fades the value label in; the ink deepens while dragging.

Accessibility

  • The thumb is role="slider" with aria-valuenow, aria-valuemin, aria-valuemax and aria-orientation.
  • Name the thumb with aria-label or aria-labelledby; marks are hidden from screen readers, which read aria-valuenow; while shown, the value label is the thumb’s aria-describedby.

API

Props

Prop
Type
Default
Description
modelValue
number
—
The current value, min when unbound
min
number
0
Minimum
max
number
100
Maximum
step
number
1
Step
dir
'ltr' | 'rtl'
—
Direction; inherited when omitted
marks
Array<{ value: number; label?: string }>
—
Ticks
label
'auto' | 'always' | 'none'
'auto'
How the value label shows
format
(value: number) => string
—
Text of the value label, locale-formatted by default
size
'sm' | 'md' | 'lg'
'md'
Size
disabled
boolean
false
Whether the slider is disabled
class
string
—
Classes appended to the root element

Events

Event
Payload
Description
update:modelValue
value: number
The value changed, continuously while dragging
commit
value: number
A drag or a key press ended