Un groupe de boutons piloté par une seule valeur : un contrôle segmenté pour un choix, ou un jeu de filtres pour plusieurs. Chaque item est un VButton, donc les tons et les variantes sont ceux que vous connaissez déjà.
itemVariant peint les éléments qui ne sont pas choisis, transparents en ghost et contourés en outline. tone colore l'élément choisi et rien d'autre, avec les trois valeurs qu'offre un bouton.
vue
itemVariant ghost
itemVariant outline
tone accent
tone neutral
tone danger
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem, type ToggleItemVariant, type ToggleTone } from 'vectis-ui'
const itemVariants: ToggleItemVariant[] = ['ghost', 'outline']
const tones: ToggleTone[] = ['accent', 'neutral', 'danger']
const byVariant = ref<Record<string, string>>({ ghost: 'week', outline: 'week' })
const byTone = ref<Record<string, string>>({ accent: 'week', neutral: 'week', danger: 'week' })
</script>
<template>
<div class="demo">
<!-- `itemVariant` paints the items that are NOT chosen: transparent under `ghost`,
outlined under `outline`. What the chosen one takes is a separate decision. -->
<div v-for="variant in itemVariants" :key="variant" class="row">
<p class="caption">itemVariant {{ variant }}</p>
<VToggle v-model="byVariant[variant]" :item-variant="variant" :label="`Period, ${variant}`">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
<!-- The tone colours the chosen item and nothing else: the rest stay neutral,
since only one of them is making a claim. -->
<div v-for="tone in tones" :key="tone" class="row">
<p class="caption">tone {{ tone }}</p>
<VToggle
v-model="byTone[tone]"
:tone="tone"
item-variant="outline"
:label="`Period, ${tone}`"
>
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Comment la sélection est dessinée
selectedVariant est la façon dont l'élément choisi est peint dans le ton du groupe : plein en solid, teinté en soft, ou par la seule couleur de son texte en ghost.
vue
solid
soft
ghost
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem, type ToggleSelectedVariant } from 'vectis-ui'
const variants: ToggleSelectedVariant[] = ['solid', 'soft', 'ghost']
const selected = ref<Record<string, string>>({ solid: 'week', soft: 'week', ghost: 'week' })
</script>
<template>
<div class="demo">
<!-- How the chosen item is drawn, in the group's tone: filled with `solid`, tinted
with `soft`, or the colour of its text alone with `ghost`. Solid is the loudest
and the safest default; ghost is for a row that must stay quiet, where the
difference rests on the text colour alone. -->
<div v-for="variant in variants" :key="variant" class="row">
<p class="caption">{{ variant }}</p>
<VToggle
v-model="selected[variant]"
:selected-variant="variant"
item-variant="outline"
:label="`Period, ${variant}`"
>
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Tailles
size reprend l'échelle partagée par tous les contrôles, de 24 à 56 pixels, et compact lui retire 4px. Elle se pose une fois sur le groupe.
vue
xs
sm
md
lg
xl
md compact
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem, type ToggleSize } from 'vectis-ui'
const sizes: ToggleSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
const selected = ref<Record<string, string>>({
xs: 'week',
sm: 'week',
md: 'week',
lg: 'week',
xl: 'week',
compact: 'week',
})
</script>
<template>
<div class="demo">
<!-- The scale shared by every control, set once on the group: each item is a
button of that size, so a toggle row and a button beside it line up. -->
<div v-for="size in sizes" :key="size" class="row">
<p class="caption">{{ size }}</p>
<VToggle v-model="selected[size]" :size="size" :label="`Period, ${size}`">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
<div class="row">
<p class="caption">md compact</p>
<VToggle v-model="selected.compact" size="md" compact label="Period, compact">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Ce que porte un item
Un élément accepte un libellé, une icône à chaque extrémité, ou le slot par défaut pour ce qu'une chaîne ne peut pas porter. Un élément réduit à son icône demande toujours un label, qui est alors son nom accessible.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VBadge, VToggle, VToggleItem } from 'vectis-ui'
import {
audio_file as audioFile,
image,
info,
search,
video_file as videoFile,
} from 'vectis-ui/icons'
const kind = ref('images')
const view = ref('list')
</script>
<template>
<div class="demo">
<!-- A label, an icon at either end, or both. The icon at the end is for what the
item carries rather than what it is. -->
<VToggle v-model="kind" item-variant="outline" label="Media">
<VToggleItem value="images" label="Images" :icon-start="image" />
<VToggleItem value="video" label="Video" :icon-start="videoFile" />
<VToggleItem value="audio" label="Audio" :icon-start="audioFile" :icon-end="info" />
</VToggle>
<!-- An item reduced to its icon still has to say what it is: with no label there
is no accessible name left, so one is given here. -->
<VToggle v-model="view" item-variant="outline" label="View">
<VToggleItem value="list" :icon-start="search" aria-label="Search" />
<VToggleItem value="grid" :icon-start="image" aria-label="Thumbnails" />
<!-- The default slot replaces the label, for anything a string cannot hold. -->
<VToggleItem value="flagged">
<VBadge :count="4">Flagged</VBadge>
</VToggleItem>
</VToggle>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
</style>
Icônes pleines
selectedIconFilled dessine l'icône de début de l'élément choisi dans sa forme pleine. iconFilled posé sur un élément remplit ses icônes quel que soit le choix. Rien n'arrive à une icône qui n'a pas de dessin plein.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
import { audio_file as audioFile, image, video_file as videoFile } from 'vectis-ui/icons'
const kind = ref('images')
</script>
<template>
<!-- The chosen item draws its icon in the filled form, a common way of reinforcing
which one is in effect, and the one that reads without relying on colour alone.
It names the icon standing FOR the item, so only the one at the start is
switched; a trailing icon keeps its outline. Nothing happens to an icon that has
no filled form, the library shipping a second drawing only where the fill really
changes the geometry. -->
<VToggle v-model="kind" selected-icon-filled item-variant="outline" label="Media">
<VToggleItem value="images" label="Images" :icon-start="image" />
<VToggleItem value="video" label="Video" :icon-start="videoFile" />
<VToggleItem value="audio" label="Audio" :icon-start="audioFile" />
</VToggle>
</template>
Détaché
detached laisse les éléments en boutons séparés avec un écart entre eux, au lieu de les joindre en un contrôle segmenté.
vue
joined, the default
detached
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const joined = ref('week')
const detached = ref('week')
</script>
<template>
<div class="demo">
<!-- Joined by default: the items melt into one segmented control, which is what
says they are one choice. -->
<div class="row">
<p class="caption">joined, the default</p>
<VToggle v-model="joined" item-variant="outline" label="Period, joined">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
<!-- Detached leaves them as separate buttons with a gap, which suits a row of
filters that happen to share a model more than it suits one exclusive choice. -->
<div class="row">
<p class="caption">detached</p>
<VToggle v-model="detached" detached item-variant="outline" label="Period, detached">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Sans coutures
seamless retire les traits dessinés entre deux éléments : la rangée se lit alors comme un cadre unique portant une surbrillance qui se déplace. Il est sans effet sous detached.
vue
with the seams
seamless
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const segmented = ref('week')
const seamless = ref('week')
</script>
<template>
<div class="demo">
<!-- Joined, a line is drawn between two items: the row reads as a set of segments,
each one of them a target. -->
<div class="row">
<p class="caption">with the seams</p>
<VToggle v-model="segmented" item-variant="outline" label="Period, segmented">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
<!-- Seamless takes those lines out, and the row reads as one frame holding a
highlight that moves. It has no effect under `detached`, where the items are
separate buttons already. -->
<div class="row">
<p class="caption">seamless</p>
<VToggle v-model="seamless" seamless item-variant="outline" label="Period, seamless">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Pleine largeur
fullWidth étire la rangée sur toute la largeur de son parent et donne à chaque élément une part égale de cette largeur.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const natural = ref('week')
const filling = ref('week')
</script>
<template>
<div class="column">
<VToggle v-model="natural" item-variant="outline" label="Period, as wide as its labels">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
<!-- Every item takes an equal share of the width, whatever its label measures. -->
<VToggle v-model="filling" full-width item-variant="outline" label="Period, filling the column">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</template>
<style scoped>
/* A column narrower than the page, so that filling it is something to see. */
.column {
display: flex;
flex-direction: column;
align-items: start;
gap: var(--vectis-space-3);
inline-size: 320px;
}
</style>
Surélevé
elevated soulève la rangée, l'ombre appartenant à la rangée plutôt qu'à chaque élément.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const period = ref('week')
</script>
<template>
<!-- The row is raised off the page, and the shadow belongs to the row rather than to
each item: segments overlap by a pixel, so a shadow per item would fall on its
neighbour and fill every joint with a dark band instead of lifting one object.
The whole row rises together on hover for the same reason. -->
<VToggle v-model="period" elevated item-variant="ghost" label="Period">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</template>
Orientation
orientation à vertical empile les éléments vers le bas, les jonctions et les flèches suivant l'axe.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const joined = ref('week')
const detached = ref('week')
</script>
<template>
<div class="demo">
<!-- Down the page instead of across. The joins move to the horizontal edges, the
corners are carved at the top and the bottom of the column, and the arrow keys
follow the axis. -->
<VToggle v-model="joined" orientation="vertical" item-variant="outline" label="Period">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
<VToggle
v-model="detached"
orientation="vertical"
detached
item-variant="outline"
label="Period, detached"
>
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</template>
<style scoped>
.demo {
display: flex;
align-items: start;
gap: var(--vectis-space-8);
}
</style>
Choisir plusieurs items
multiple permet de choisir plusieurs éléments à la fois, ce qui fait de la valeur une liste. Cliquer un élément choisi le rend, et le tableau n'est jamais muté sur place.
vue
images, video
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
import {
audio_file as audioFile,
description,
image,
video_file as videoFile,
} from 'vectis-ui/icons'
// In multiple mode the value is a list, and it is never mutated in place: each change
// hands back a new array, which is what wakes a watcher bound to it.
const kinds = ref(['images', 'video'])
</script>
<template>
<div class="demo">
<!-- Several items at once, which turns the exclusive choice into a set of filters.
Clicking a chosen item gives it up again. -->
<VToggle v-model="kinds" multiple item-variant="outline" selected-icon-filled label="Media">
<VToggleItem value="images" label="Images" :icon-start="image" />
<VToggleItem value="video" label="Video" :icon-start="videoFile" />
<VToggleItem value="audio" label="Audio" :icon-start="audioFile" />
<VToggleItem value="documents" label="Documents" :icon-start="description" />
</VToggle>
<p class="value">{{ kinds.length ? kinds.join(', ') : 'Nothing selected' }}</p>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-3);
}
.value {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Garder une sélection
mandatory refuse d'abandonner le dernier élément choisi. C'est une garde et rien de plus : il ne sélectionne rien de lui-même.
vue
one choice
several, the last one held
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const period = ref('week')
const kinds = ref(['images'])
</script>
<template>
<div class="demo">
<!-- Clicking the chosen item normally gives it up, leaving nothing selected.
`mandatory` refuses that last step, so a row that has an answer keeps one. It
is a guard and nothing more: it selects nothing on its own at the start, and a
group that begins empty stays empty until something is clicked. -->
<div class="row">
<p class="caption">one choice</p>
<VToggle v-model="period" mandatory item-variant="outline" label="Period">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
<!-- With several, it is the last remaining one that cannot be given up. -->
<div class="row">
<p class="caption">several, the last one held</p>
<VToggle v-model="kinds" multiple mandatory item-variant="outline" label="Media">
<VToggleItem value="images" label="Images" />
<VToggleItem value="video" label="Video" />
<VToggleItem value="audio" label="Audio" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Désactivé
disabled vaut pour tout le groupe, où rien ne répond et où aucun élément ne prend le focus, ou pour un seul élément, que les flèches enjambent alors. Les deux se grisent par les tokens de couleur.
vue
the group
one item
<script setup lang="ts">
import { ref } from 'vue'
import { VToggle, VToggleItem } from 'vectis-ui'
const group = ref('week')
const item = ref('day')
</script>
<template>
<div class="demo">
<!-- The whole group at once: nothing responds and no item takes focus. -->
<div class="row">
<p class="caption">the group</p>
<VToggle v-model="group" disabled item-variant="outline" label="Period, disabled">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
<!-- Or a single item, which stops responding and which the arrow keys step over as
though it were not in the row. Both grey out through the colour tokens rather
than through an opacity, so they keep their contrast. -->
<div class="row">
<p class="caption">one item</p>
<VToggle v-model="item" item-variant="outline" label="Period">
<VToggleItem value="day" label="Day" />
<VToggleItem value="week" label="Week" disabled />
<VToggleItem value="month" label="Month" />
</VToggle>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
API
Props
VToggle
Prop
Type
Défaut
multiple
boolean
false
Permet de choisir plusieurs items à la fois, ce qui fait de la valeur une liste.
mandatory
boolean
false
Refuse d'abandonner le dernier item choisi, pour que quelque chose reste toujours sélectionné une fois que ça l'a été. C'est un garde-fou et rien de plus : il ne sélectionne rien de lui-même au départ.
detached
boolean
false
Laisse les items en boutons séparés avec un écart entre eux. Sans lui, ils sont assemblés en un seul contrôle segmenté.
seamless
boolean
false
Retire les traits entre les items assemblés, si bien que la rangée se lit comme un cadre unique plutôt que comme des segments. Sans effet sous detached, où les items sont déjà des boutons séparés.
orientation
ToggleOrientation'horizontal' | 'vertical'
'horizontal'
Si les items courent en travers de la page ou de haut en bas.
fullWidth
boolean
false
Étire la rangée sur toute la largeur de son parent, chaque item en prenant une part égale, aux conditions de la prop de VButtonGroup.
itemVariant
ToggleItemVariant'ghost' | 'outline'
'ghost'
Comment sont dessinés les items non sélectionnés. Ce que prend le sélectionné, c'est selectedVariant. Elle porte le nom des items parce que c'est ce qu'elle peint : sur VTabs et VDataTable, variant nomme la décoration du cadre.
selectedVariant
ToggleSelectedVariant'solid' | 'soft' | 'ghost'
'solid'
Comment l'item sélectionné est dessiné, dans le ton du groupe : plein avec solid, teinté avec soft, ou la couleur de son seul texte avec ghost.
tone
ToggleTone'accent' | 'neutral' | 'danger'
'accent'
La couleur que prend un item sélectionné. Les autres restent neutres.
size
ToggleSize'xs' | 'sm' | 'md' | 'lg' | 'xl'
'md'
La hauteur des items, tirée de l'échelle partagée par tous les contrôles.
compact
boolean
false
Retire 4px à la hauteur de chaque item.
elevated
boolean
false
Soulève la rangée, aux conditions de la prop de VButtonGroup : l'ombre appartient à la rangée et non à chaque item, ce qui garde les joints nets.
disabled
boolean
false
Rend tout le groupe inutilisable.
selectedIconFilled
boolean
false
Dessine l'icône de l'item sélectionné dans sa forme pleine, une façon courante de renforcer qu'il est celui en vigueur.
label
string
aucune
Ce que les lecteurs d'écran annoncent pour le groupe, « Alignement du texte », « Filtres ». Vivement recommandé : aucune valeur par défaut ne pourrait dire à quoi sert un groupe de boutons.
Ce qui est sélectionné, et sa FORME suit multiple : une valeur unique, ou null d'où elle part, quand un seul item peut être choisi, et un tableau quand plusieurs le peuvent. Une valeur nulle ou scalaire passée en mode multiple est lue comme une sélection vide. Le tableau n'est jamais muté sur place. Recliquer l'item sélectionné le désélectionne, sauf si mandatory est posé.
Une icône après le libellé. Elle ne passe pas à sa forme pleine sous l'effet du selectedIconFilled du groupe, qui nomme l'icône représentant l'item et non celle qui le suit ; iconFilled la remplit pour de bon.
iconFilled
boolean
false
Rend iconStart et iconEnd dans leur forme pleine, que l'item soit sélectionné ou non. Le selectedIconFilled du groupe remplit toujours l'icône de début de l'item sélectionné quand celle-ci est omise.
disabled
boolean
false
Rend cet item inutilisable : il ne répond plus, les flèches l'enjambent, et il se grise par les tokens de couleur.
Slots
VToggle
Slot
Type
default
{}
Les items du groupe.
VToggleItem
Slot
Type
default
{}
Le contenu de l'item, qui remplace la prop label.
start
{}
Un contenu avant le libellé, qui prend la place de iconStart.
end
{}
Un contenu après le libellé, qui prend la place de iconEnd.
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.