Skip to content

Sliders

A slider is an input where the user selects a value from within a given range. Sliders typically have a slider thumb that can be moved along a bar, rail, or track to change the value of the slider.

Features

  • Labeling, descriptions, and error message displays are automatically linked to input and label elements with aria-* attributes.
  • v-model support for binding the value of the slider and the individual thumbs.
  • Multi-thumb support with auto value clamping.
  • Support for min, max, and step attributes.
  • Support for both horizontal and vertical orientations.
  • Support for both LTR and RTL directions.
  • Validation with Standard Schema.
  • Interactive behaviors:
    • Clicking the track element sets the value of the slider or the nearest suitable thumb to the clicked position.
    • Dragging the thumb element changes the value of the slider.
  • Supported keyboard interactions:
KeyDescription
Right
Increments the slider value of the currently focused thumb. In RTL, it decrements instead.
Left
Decrements the slider value of the currently focused thumb. In RTL, it increments instead.
Up
Increments the slider value of the currently focused thumb.
Down
Decrements the slider value of the currently focused thumb.
Home
Sets the slider value of the currently focused thumb to the minimum possible value.
End
Sets the slider value of the currently focused thumb to the maximum possible value.
Page Up
Increments the slider value of the currently focused thumb by a large step.
Page Down
Decrements the slider value of the currently focused thumb by a large step.

Anatomy

Label
Label
74
Output
Track
Thumb
Group

Building a thumb component

Every slider needs at least one thumb to represent the current value of the slider. This means the slider in Formwerk is a compound component similar to radio buttons.

First, let’s build a thumb component that will be used in the slider later. We will keep styling to a minimum by using a simple circle SVG.

You will be using the useSliderThumb composable to build the thumb component.

<template>
<div v-bind="thumbProps" class="thumb">
<div class="tooltip">{{ currentValue }}</div>
</div>
</template>
<script setup lang="ts">
import { useSliderThumb, type SliderThumbProps } from '@formwerk/core';
const props = defineProps<SliderThumbProps>();
const { thumbProps, currentValue } = useSliderThumb(props);
</script>
43 collapsed lines
<style scoped>
.thumb {
background: #10b981;
width: 18px;
height: 18px;
border-radius: 999999px;
&:hover {
background: #16a34a;
}
.tooltip {
position: absolute;
border-radius: 6px;
display: none;
font-size: 14px;
color: #fff;
background: #10b981;
left: 50%;
top: 100%;
translate: -50% 10%;
padding: 6px;
}
&:focus,
&:active {
outline: none;
background: #047857;
.tooltip {
display: block;
}
}
&[aria-orientation='vertical'] {
.tooltip {
left: 100%;
top: 50%;
translate: 10% -50%;
}
}
}
</style>

There is nothing to show yet because we need to build the slider component.

Building a slider component

You will be using the useSlider composable to build the slider component.

The useSlider composable returns binding objects for the elements shown in the anatomy. You will use v-bind to bind them to the corresponding DOM elements.

The Slider is what we consider a fully custom component, meaning it doesn’t have an underlying input base element that you can use. While the input[type="range"] may be a suitable candidate, it doesn’t scale to support the wide range (pun intended) of use-cases that developers expect of slider inputs today.

<script setup lang="ts">
import Slider from './Slider.vue';
</script>
<template>
<Slider label="Volume" />
</template>
<script setup lang="ts">
import { useSlider, type SliderProps } from '@formwerk/core';
import Thumb from './Thumb.vue';
const props = defineProps<SliderProps>();
const {
trackProps,
groupProps,
labelProps,
errorMessage,
errorMessageProps,
useThumbMetadata,
} = useSlider(props);
const thumbData = useThumbMetadata(0);
</script>
<template>
<div class="slider" v-bind="groupProps">
<div v-bind="labelProps" class="slider-label">{{ label }}</div>
<div v-bind="trackProps" class="track">
<Thumb />
</div>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error">
{{ errorMessage }}
</div>
</div>
</template>
62 collapsed lines
<style scoped>
.slider {
--color-text: #333;
--color-error: #f00;
display: flex;
align-items: center;
gap: 6px 14px;
flex-wrap: wrap;
.slider-label {
margin-bottom: 0.25rem;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.track {
display: flex;
align-items: center;
width: 150px;
height: 6px;
background-color: #a1a1aa;
border-radius: 6px;
&::before {
content: '';
width: calc(v-bind('thumbData.percent') * 100%);
background-color: #10b981;
border-radius: 6px;
height: 6px;
}
}
.error {
display: none;
font-size: 13px;
color: #f00;
width: 100%;
}
&:has([aria-invalid='true']) {
.error {
display: block;
}
}
&:has([aria-orientation='vertical']) {
.track {
flex-direction: column;
margin: 0;
height: 80px;
width: 6px;
&::before {
height: calc(v-bind('thumbData.percent') * 100%);
width: 6px;
margin-top: auto;
}
}
}
}
</style>

Notice that in order to model the slider progress visually, we used the utility composable useThumbMetadata to calculate the percentage of the thumb position.

Validation

Because sliders in Formwerk are a fully custom component, they don’t support any HTML validation attributes. You can, however, use Standard Schema to validate the value of the slider.

Standard Schema

The useSlider composable accepts a schema prop that is an instance of a Standard Schema. This includes multiple providers like Zod, Valibot, Arktype, and more.

<script setup lang="ts">
import { z } from 'zod';
import Slider from './Slider.vue';
const schema = z.number('Required').min(30).max(80);
</script>
<template>
<Slider label="Volume" :schema="schema" />
</template>

Usage

Multiple Thumbs

You can have as many thumbs as you want inside a slider. Just add another <Thumb /> component, and this will be automatically handled for you:

  • Min/Max clamping for each thumb.
  • Value conversion to an array instead of a single number.
<script setup lang="ts">
import { ref } from 'vue';
import MultipleSlider from './MultipleSlider.vue';
const value = ref([10, 30]);
</script>
<template>
<MultipleSlider label="Multiple Slider" v-model="value" />
MultipleSlider value is: {{ value }}
</template>
<script setup lang="ts">
import { useSlider, type SliderProps } from '@formwerk/core';
import Thumb from './Thumb.vue';
const props = defineProps<SliderProps>();
const {
trackProps,
groupProps,
labelProps,
errorMessage,
errorMessageProps,
useThumbMetadata,
} = useSlider(props);
const t1 = useThumbMetadata(0);
const t2 = useThumbMetadata(1);
</script>
<template>
<div class="slider" v-bind="groupProps">
<div v-bind="labelProps">{{ label }}</div>
<div v-bind="trackProps" class="track">
<Thumb />
<Thumb />
</div>
<div v-if="errorMessage" v-bind="errorMessageProps">
{{ errorMessage }}
</div>
</div>
</template>
27 collapsed lines
<style scoped>
.slider {
--track-width: 150px;
.track {
display: flex;
align-items: center;
width: var(--track-width);
margin-top: 24px;
margin-bottom: 24px;
height: 6px;
background-color: #a1a1aa;
border-radius: 6px;
&::before {
content: '';
width: calc(
(v-bind('t2.sliderPercent') - v-bind('t1.sliderPercent')) * 100%
);
background-color: #10b981;
border-radius: 6px;
height: 6px;
translate: calc(v-bind('t1.sliderPercent') * var(--track-width)) 0;
}
}
}
</style>

Note that we did change some CSS from the previous examples in order to color the track properly, but it is up to you how you want to do that in any way you want.

Disabled

Use disabled to mark sliders as non-interactive. Disabled sliders are not validated and are not submitted.

<script setup lang="ts">
import { ref } from 'vue';
import Slider from './Slider.vue';
const value = ref(50);
</script>
<template>
<Slider label="Disabled Slider" v-model="value" disabled />
</template>

You can also disable any individual Thumb by passing disabled to it as well. But it won’t prevent validation/submission of the slider.

Readonly

Readonly sliders are validated and submitted, but they do not accept user input. The slider thumbs would still be focusable. For more info, check the MDN.

<script setup lang="ts">
import { ref } from 'vue';
import Slider from './Slider.vue';
const value = ref(50);
</script>
<template>
<Slider label="Readonly Slider" v-model="value" readonly />
</template>

RTL

The useSlider composable accepts a dir property. You can set it to RTL, and it will handle thumb positioning automatically along with inverting the horizontal arrow keys (left/right arrows).

<script setup lang="ts">
import { ref } from 'vue';
import Slider from './Slider.vue';
const value = ref(50);
</script>
<template>
<Slider label="Vertical Slider" v-model="value" dir="rtl" />
</template>

Orientation

useSlider also accepts an orientation prop. You can set it to either horizontal (default) or vertical.

Formwerk will handle most things in either orientation in terms of interaction and thumb positioning. But slider layout is left to you to do.

Here is an example that uses the [aria-orientation] attribute that is applied automatically. We will use it to flip the slider we initially created with some custom styles.

<script setup lang="ts">
import { ref } from 'vue';
import Slider from './Slider.vue';
const value = ref(0);
</script>
<template>
<Slider label="Vertical Slider" v-model="value" orientation="vertical" />
</template>
<script setup lang="ts">
import { useSlider, type SliderProps } from '@formwerk/core';
import Thumb from './Thumb.vue';
const props = defineProps<SliderProps>();
const {
trackProps,
groupProps,
labelProps,
errorMessage,
errorMessageProps,
useThumbMetadata,
} = useSlider(props);
const thumbData = useThumbMetadata(0);
</script>
<template>
<div class="slider" v-bind="groupProps">
<div v-bind="labelProps" class="slider-label">{{ label }}</div>
<div v-bind="trackProps" class="track">
<Thumb />
</div>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error">
{{ errorMessage }}
</div>
</div>
</template>
62 collapsed lines
<style scoped>
.slider {
--color-text: #333;
--color-error: #f00;
display: flex;
align-items: center;
gap: 6px 14px;
flex-wrap: wrap;
.slider-label {
margin-bottom: 0.25rem;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.track {
display: flex;
align-items: center;
width: 150px;
height: 6px;
background-color: #a1a1aa;
border-radius: 6px;
&::before {
content: '';
width: calc(v-bind('thumbData.percent') * 100%);
background-color: #10b981;
border-radius: 6px;
height: 6px;
}
}
.error {
display: none;
font-size: 13px;
color: #f00;
width: 100%;
}
&:has([aria-invalid='true']) {
.error {
display: block;
}
}
&:has([aria-orientation='vertical']) {
.track {
flex-direction: column;
margin: 0;
height: 80px;
width: 6px;
&::before {
height: calc(v-bind('thumbData.percent') * 100%);
width: 6px;
margin-top: auto;
}
}
}
}
</style>

Discrete Slider Values

You can also use discrete slider values by passing an options prop to the slider. This is useful for non-numeric values like a rating slider.

When doing this, the slider min/max will be set to the first and last option, and the step will be set to the difference between the first and last option divided by the number of options you want to show. This also means the step prop is ignored when options is used.

<script setup lang="ts">
import Slider from './Slider.vue';
const options = ['Bad', 'Poor', 'Okay', 'Good', 'Great'];
</script>
<template>
<Slider label="Discrete Slider" :options="options" />
</template>

Generic Types

The slider’s SliderProps accepts a generic type parameter that allows you to build generically typed slider components.

<script setup lang="ts" generic="TValue">
import { useSlider, type SliderProps } from '@formwerk/core';
const props = defineProps<SliderProps<TValue>>();
</script>

This gives you auto type inference for the slider’s value even when using the options prop for any type of value.

Styling

You probably have noticed that the trackProps and thumbProps contain some minor styling properties. These are the bare minimum to get the slider working, and they are automatically added for you.

If you are interested in knowing what properties are added, here is a list:

  • trackProps style properties:
    • container-type: is set to size or inline-size depending on the orientation. You should NOT override this property.
    • position: is set to relative. You can override this to anything but static.
  • thumbProps style properties:
    • position: is set to absolute. You should NOT override this property.
    • translate: used to position the thumb. You should NOT override this property.
    • will-change: is set to translate. You should NOT override this property.
    • Any inset position properties like top, left, right, bottom are set to 0. You should NOT override these properties.

We were very careful to not add any easily overridden properties to the trackProps and thumbProps to avoid any conflicts with your custom styles.

Other than all of that, you can use any styling solution you want, whether it’s TailwindCSS or plain CSS.

Thumb attributes

The thumb element will receive the aria-orientation attribute, which is the same as the slider’s orientation, so you can style tooltips or other UI elements according to the slider’s orientation.

Slider attributes

The slider element will receive the aria-orientation attribute, which is the prop you pass to the slider.

API

Thumb

Props

These are the properties that can be passed to the useSliderThumb composable.

NameTypeDescription
disabledWhether the slider thumb is disabled.
formatValueA function to format the value text of the slider thumb. Used for setting the `aria-valuetext` attribute.
labelThe label text for the slider thumb.
modelValueThe v-model value of the slider thumb.

Returns

These are the properties in the object returned by the useSliderThumb composable.

NameTypeDescription
currentTextThe current formatted value of the thumb.
currentValueThe current value of the thumb.
isDisabledWhether the thumb is disabled.
isDraggingWhether the thumb is currently being dragged.
thumbElReference to the thumb element.
thumbPropsProps for the thumb element.

Slider

Props

These are the properties that can be passed to the useSlider composable.

NameTypeDescription
dirThe text direction of the slider (ltr or rtl).
disabledWhether the slider is disabled.
labelThe label text for the slider.
maxThe maximum value allowed for the slider. Ignored if `options` is provided.
minThe minimum value allowed for the slider. Ignored if `options` is provided.
modelValueThe v-model value of the slider.
nameThe name attribute for the slider input.
optionsDiscrete values that the slider can accept.
orientationThe orientation of the slider (horizontal or vertical).
readonlyWhether the slider is readonly.
schemaSchema for slider validation.
stepThe step increment between values. Ignored if `options` is provided.
valueThe value attribute of the slider input.

Returns

These are the properties in the object returned by the useSlider composable.

NameTypeDescription
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
Props for the error message element.
errorsThe errors for the field.
fieldValueThe value of the field.
groupProps
Ref<{ id: string; role: string; dir: Direction; 'aria-label'?: string; 'aria-labelledby'?: string; }>
Props for the root element for the slider component.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsProps for the label element.
outputPropsProps for the output element.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
trackElReference to the track element.
trackProps
Ref<{ style: CSSProperties; onMousedown(e: MouseEvent): void; }>
Props for the track element.
useThumbMetadata

useThumbMetadata returns

Additionally, useThumbMetadata returns a computed object with the following properties:

NameTypeDescription
maxThe maximum value of the slider.
minThe minimum value of the slider.
percentThe percent of the slider that the thumb is at.
sliderMaxThe maximum value of the slider.
sliderMinThe minimum value of the slider.
sliderPercentThe percent of the slider that the thumb is at.
valueThe current value of the thumb.