Raccourci clavier : Ctrl + K
Commencer

Curseur

Une valeur choisie en glissant, avec une poignée ou deux. Il est bâti sur de vrais champs range : le clavier, le formulaire et les technologies d'assistance viennent donc tous du navigateur.

Utilisation

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const volume = ref(40)
</script>

<template>
  <div class="demo">
    <VSlider v-model="volume" label="Volume" />
  </div>
</template>

<style scoped>
.demo {
  inline-size: 20rem;
}
</style>

Exemples

Intervalle

range offre deux poignées pour choisir une plage, ce qui fait de la valeur une paire. La paire reste ordonnée, une poignée emmenée au-delà de sa voisine la poussant devant elle, et chacune est annoncée comme le début ou la fin de la plage.

vue

20 to 60

<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const budget = ref<[number, number]>([20, 60])
</script>

<template>
  <div class="demo">
    <!-- `range` is what settles the mode, and the model then holds a pair. A thumb taken
         past its sibling pushes it along, so the pair stays ordered whatever the reader
         does and a range closed onto a single value can still be opened again. Each thumb
         is announced as the start or the end of the range rather than as two sliders with
         the same name. -->
    <VSlider v-model="budget" range label="Budget" />
    <p class="value">{{ budget[0] }} to {{ budget[1] }}</p>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
  inline-size: 20rem;
}
.value {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Minimum et maximum

min et max bornent la valeur, 0 et 100 sauf indication contraire, négatifs compris. Tout le reste se mesure par rapport à eux.

vue

Temperature, -10 to 40

19 °C

Rating, 1 to 5

3 out of 5

<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const temperature = ref(19)
const rating = ref(3)
</script>

<template>
  <div class="demo">
    <!-- The bounds are 0 and 100 by default, and both may be anything, negatives
         included. -->
    <div class="row">
      <p class="caption">Temperature, -10 to 40</p>
      <VSlider v-model="temperature" :min="-10" :max="40" label="Temperature" />
      <p class="value">{{ temperature }} °C</p>
    </div>

    <!-- A short span is what makes each stop reachable from the keyboard in a few
         presses, and what makes the ticks worth drawing. -->
    <div class="row">
      <p class="caption">Rating, 1 to 5</p>
      <VSlider v-model="rating" :min="1" :max="5" ticks label="Rating" />
      <p class="value">{{ rating }} out of 5</p>
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 20rem;
}
.row {
  display: grid;
  gap: var(--vectis-space-2);
}
.caption,
.value {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Pas

step est l'écart entre deux valeurs sur lesquelles la poignée peut s'arrêter, et le pas d'une flèche du clavier. ticks marque ces arrêts sur le rail, et au-delà de cinquante pas aucune graduation n'est dessinée.

vue

step 10, with ticks

step 0.1

0.4

0 to 95 by 10, so the last stop is 90

50

<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const quantity = ref(50)
const opacity = ref(0.4)
const odd = ref(50)
</script>

<template>
  <div class="demo">
    <!-- The step is the gap between two values the thumb can stop on, and it is also
         what an arrow key moves by. `ticks` marks each of those stops on the track. -->
    <div class="row">
      <p class="caption">step 10, with ticks</p>
      <VSlider v-model="quantity" :step="10" ticks label="Quantity" />
    </div>

    <!-- A fractional step is fine: the value is rounded back onto the step, so a tenth
         does not accumulate the error floating point leaves behind. -->
    <div class="row">
      <p class="caption">step 0.1</p>
      <VSlider v-model="opacity" :min="0" :max="1" :step="0.1" ticks label="Opacity" />
      <p class="value">{{ opacity }}</p>
    </div>

    <!-- When the span does not divide evenly by the step, the last stop falls short of
         the maximum: here 0 to 95 by 10 stops at 90, and the ticks say so rather than
         drawing one where the thumb cannot go. Past fifty steps no tick is drawn at
         all, a comb that dense being unreadable. -->
    <div class="row">
      <p class="caption">0 to 95 by 10, so the last stop is 90</p>
      <VSlider v-model="odd" :max="95" :step="10" ticks label="Coverage" />
      <p class="value">{{ odd }}</p>
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 20rem;
}
.row {
  display: grid;
  gap: var(--vectis-space-2);
}
.caption,
.value {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Libellés texte

labels nomme les arrêts sous le rail, une entrée par pas, et c'est ce qu'annonce un lecteur d'écran à la place du nombre brut. Donner des libellés active les graduations de lui-même.

vue
XSSMLXL
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const size = ref(2)
</script>

<template>
  <div class="demo">
    <!-- One label per step, in order. They name the stops on the track, and they also
         become what a screen reader announces in place of the raw number: this slider
         says "M" and not "2". Giving labels turns the ticks on by itself, a label with
         no mark to sit under having nothing to point at. -->
    <VSlider v-model="size" :min="0" :max="4" :labels="['XS', 'S', 'M', 'L', 'XL']" label="Size" />
  </div>
</template>

<style scoped>
.demo {
  inline-size: 20rem;
  padding-inline: var(--vectis-space-4);
}
</style>

Libellés icône

Un libellé peut porter une icône au lieu d'un mot, le libellé restant ce qui est annoncé et ce comme quoi la valeur est lue. Les deux formes se mélangent dans la même liste.

vue

Information

<script setup lang="ts">
import { ref } from 'vue'
import { VSlider, type SliderLabel } from 'vectis-ui'
import { check_circle as checkCircle, error, info, warning } from 'vectis-ui/icons'

const threshold = ref(1)

// A label may be an icon instead of a word, and then the words come with it: the icon
// is what the reader sees, the label what a screen reader announces.
const labels = [
  { icon: checkCircle, label: 'Silent' },
  { icon: info, label: 'Information' },
  { icon: warning, label: 'Warning' },
  { icon: error, label: 'Error' },
] satisfies SliderLabel[]
</script>

<template>
  <div class="demo">
    <VSlider v-model="threshold" :min="0" :max="3" :labels="labels" label="Alert threshold" />
    <p class="value">{{ labels[threshold]?.label }}</p>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
  inline-size: 20rem;
  padding-inline: var(--vectis-space-4);
}
.value {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Montrer la valeur pendant le glissement

tooltip affiche une bulle au-dessus de la poignée pendant le glissement ou tant qu'elle a le focus clavier, une par poignée. Elle est décorative et masquée aux technologies d'assistance.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const volume = ref(40)
const budget = ref<[number, number]>([20, 60])
</script>

<template>
  <div class="demo">
    <!-- The bubble shows the value above the thumb while it is being dragged or while
         it holds keyboard focus, which is what a track with no numbers on it needs.
         It is decorative: the value is already in the slider's own announcement, so
         nothing is said twice. -->
    <VSlider v-model="volume" tooltip label="Volume" />

    <!-- In range mode each thumb carries its own. -->
    <VSlider v-model="budget" tooltip range label="Budget" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-8);
  inline-size: 20rem;
  /* Room for the bubbles, which are drawn above the track. */
  padding-block-start: var(--vectis-space-6);
}
</style>

Saisir la valeur exactement

inputs ajoute un champ numérique à côté du rail, un par extrémité en mode plage. Ce qui est saisi est validé à la sortie du champ ou sur Entrée, ramené dans les bornes et aligné sur le pas ; une saisie illisible remet la valeur précédente.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const volume = ref(40)
const budget = ref<[number, number]>([20, 60])
</script>

<template>
  <div class="demo">
    <!-- Sliding is quick and imprecise, and this is the way out: a number field beside
         the track, one per end in range mode. What is typed is committed when the
         field is left or on Enter, never as it is typed, or the 1 of 15 would be
         clamped before the 5 was pressed. An out of bounds entry is brought back
         inside, an unreadable one puts the previous value back. -->
    <VSlider v-model="volume" inputs label="Volume" />
    <VSlider v-model="budget" inputs range label="Budget" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 26rem;
}
</style>

Taille des champs

size règle la hauteur des champs numériques qu'ajoute inputs, md par défaut comme sur tout champ. Dans un VInputGroup, c'est la rangée qui décide, comme pour ses autres champs.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const small = ref(20)
const medium = ref(40)
const large = ref(60)
</script>

<template>
  <div class="demo">
    <VSlider v-model="small" inputs size="sm" label="Small" />
    <VSlider v-model="medium" inputs label="Medium" />
    <VSlider v-model="large" inputs size="lg" label="Large" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 26rem;
}
</style>

Orientation

orientation à vertical dresse le curseur, la plus petite valeur en bas. Sa longueur vient d'un token et non de son conteneur.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const volume = ref(40)
const budget = ref<[number, number]>([20, 60])
const gain = ref(60)
</script>

<template>
  <div class="demo">
    <!-- Upright, with the lowest value at the bottom. Everything else is unchanged:
         the ticks, the labels, the bubbles and the number fields all follow the axis,
         and the keyboard is still the browser's. -->
    <VSlider v-model="volume" orientation="vertical" label="Volume" />
    <VSlider v-model="budget" orientation="vertical" range tooltip label="Budget" />

    <!-- A vertical slider has no container to take its length from, so it reads a
         token instead. Override that token to make it longer or shorter. -->
    <VSlider v-model="gain" class="tall" orientation="vertical" inputs label="Gain" />
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  align-items: start;
  gap: var(--vectis-space-8);
}
.tall {
  --vectis-control-size-slider-length: 16rem;
}
</style>

Désactivé

disabled grise le rail, la poignée et les graduations par les tokens de couleur, sort les poignées de l'ordre de tabulation et désactive les champs numériques avec elles.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const volume = ref(30)
const budget = ref<[number, number]>([20, 60])
</script>

<template>
  <div class="demo">
    <!-- It greys the track, the thumb and the ticks through the colour tokens rather
         than through an opacity, takes the thumbs out of the tab order, and disables the
         number fields along with them: there is no half-usable slider whose value can
         still be typed. -->
    <VSlider v-model="volume" disabled label="Volume" />
    <VSlider v-model="budget" range :step="10" ticks inputs disabled label="Budget" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 26rem;
}
</style>

Lecture seule

readonly montre la valeur sans permettre de la changer. Une entrée de plage n'a pas de lecture seule native, donc le composant annule les touches qui déplacent une poignée et remet en place une poignée que le pointeur a déplacée. Les poignées restent focalisables et annoncées, et les champs numériques passent en lecture seule avec elles.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const volume = ref(40)
const budget = ref<[number, number]>([20, 60])
</script>

<template>
  <div class="demo">
    <!-- The thumbs stay focusable and announced, but neither a key nor the pointer
         moves them, and the number fields turn read-only with them. -->
    <VSlider v-model="volume" readonly label="Volume" />
    <VSlider v-model="budget" range inputs readonly label="Budget" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 26rem;
}
</style>

Invalide

invalid cercle les poignées de la couleur de danger, comme une case à cocher colore sa bordure, et le signale aux technologies d'assistance par aria-invalid sur chaque poignée et chaque champ.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSlider } from 'vectis-ui'

const quota = ref<[number, number]>([30, 90])
</script>

<template>
  <div class="demo">
    <VSlider v-model="quota" range inputs invalid label="Quota" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 26rem;
}
</style>

Dans un formulaire

name, id et les aria-* sont redirigés sur le vrai input range sous-jacent. Le nommage passe par la prop label, qui pose un aria-label ; votre propre aria-label ou aria-labelledby a la priorité sur elle, et une plage nomme alors ses deux poignées à partir de ce que vous avez donné. Une plage n'a pas de valeur unique à soumettre : seule la poignée de fin porte le nom. hint dessine une ligne d'aide sous la piste et s'ajoute à votre propre aria-describedby plutôt que de le remplacer.

vue

Nothing submitted yet

<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VSlider } from 'vectis-ui'

const volume = ref(40)
const submitted = ref<string | null>(null)

function onSubmit(event: Event) {
  const data = new FormData(event.target as HTMLFormElement)
  submitted.value = [...data].map(([key, value]) => `${key}=${value}`).join(', ')
}
</script>

<template>
  <form class="demo" @submit.prevent="onSubmit">
    <!-- The root is a layout box, so `name`, `id` and the aria-* are redirected onto
         the real range input: left on the wrapper a name would submit nothing and a
         label would point at a div. Here the name reaches the form and the label
         reaches the thumb, which is why the `label` prop is left out: it sets an
         aria-label, and that would win over the visible label. -->
    <label class="field-label" for="volume">Volume</label>
    <VSlider id="volume" v-model="volume" name="volume" />

    <VButton class="submit" type="submit" size="sm" variant="outline" tone="neutral">
      Submit
    </VButton>
    <p class="log">{{ submitted ?? 'Nothing submitted yet' }}</p>
  </form>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
  inline-size: 20rem;
}
.submit {
  justify-self: start;
}
.field-label {
  font-size: var(--vectis-text-label-size);
  font-weight: var(--vectis-text-label-weight);
}
.log {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

API

Props

PropTypeDéfaut
minnumber0
La valeur la plus basse que la poignée peut atteindre.
maxnumber100
La valeur la plus haute que la poignée peut atteindre.
stepnumber1
L'écart entre deux valeurs sur lesquelles la poignée peut s'arrêter. C'est aussi le pas des flèches, et ce sur quoi une valeur saisie dans le champ voisin est alignée.
rangebooleanfalse
Propose deux poignées pour choisir un intervalle, ce qui fait de la valeur une paire.
disabledbooleanfalse
Rend le curseur inutilisable.
readonlybooleanfalse
Montre la valeur sans permettre de la changer. Les poignées restent focalisables et sont annoncées en lecture seule, mais ni le pointeur ni le clavier ne les déplacent, et les champs numériques passent en lecture seule avec elles.
invalidbooleanfalse
Marque la valeur comme invalide, ce qui colore les poignées et le signale aux technologies d'assistance. C'est pour une règle que le navigateur ne sait pas vérifier seul.
sizeSliderSize'sm' | 'md' | 'lg''md'
La hauteur des champs numériques qu'ajoute inputs. Dans un VInputGroup, la taille du groupe l'emporte, comme pour chaque champ de la rangée.
labelstringaucune
Ce que les lecteurs d'écran annoncent pour le curseur. C'est un nom accessible, rien ne s'affiche à l'écran. En mode intervalle, les deux poignées sont annoncées comme le début et la fin de celui-ci.
hintstringaucune
Une ligne d'aide sous la piste, qui dit ce que les nombres signifient ou jusqu'où ils peuvent aller. Elle est rattachée au curseur pour les technologies d'assistance, donc lue après le nom plutôt que comme une partie de celui-ci.
orientationSliderOrientation'horizontal' | 'vertical''horizontal'
Dresse le curseur à la verticale, la valeur la plus basse en bas.
inputsbooleanfalse
Ajoute un champ numérique à côté du curseur pour poser la valeur exactement, un champ ou un par extrémité en mode intervalle. Glisser est rapide mais imprécis ; c'est la porte de sortie.
ticksbooleanfalse
Marque chaque pas sur la piste. Fournir des libellés l'implique. Au-delà de cinquante pas, les marques formeraient un peigne illisible et ne sont pas dessinées du tout.
labelsSliderLabel[]aucune
Un libellé pour chaque pas, dans l'ordre : un texte, ou une icône avec les mots qui la nomment pour les lecteurs d'écran. Ils deviennent aussi ce qu'un lecteur d'écran annonce à la place du nombre brut.
tooltipbooleanfalse
Affiche la valeur dans une bulle au-dessus de la poignée pendant qu'on la déplace ou qu'elle a le focus.
v-modelSliderValue0
La valeur : un nombre unique, ou une paire ordonnée dès que range est posé. C'est cette prop, et non la forme de cette valeur, qui décide du nombre de poignées. La paire reste ordonnée quoi que fasse le lecteur, une poignée emmenée au-delà de sa voisine la poussant devant elle.

Événements

ÉvénementType
input[value: SliderValue]
La valeur est en train de changer : à chaque pas d'un glissement, et à chaque touche qui déplace une poignée. L'événement porte la valeur entière, une paire en mode intervalle, et part de l'une ou l'autre poignée.
change[value: SliderValue]
Le lecteur s'est arrêté sur une valeur : une poignée a été relâchée ou déplacée au clavier, ou un champ numérique a été validé. L'événement porte la valeur entière, une paire en mode intervalle, et part de l'une ou l'autre poignée, là où le v-model suit chaque pas d'un glissement.

Types

Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender
export type SliderLabel = string | { icon: IconSource; label: string }
export type SliderValue = number | [number, number]

Variables CSS

TokenValeur
--vectis-control-size-slider-track0.375rem
--vectis-control-size-slider-thumb1.25rem
--vectis-control-size-slider-length10rem
--vectis-control-size-slider-field5rem