Une barre d'onglets et les panneaux qu'ils montrent. Les panneaux sont un slot optionnel : le même composant sert donc de simple barre ou de contrôle segmenté quand il n'y a rien à révéler.
Utilisation
vue
What the project is for.
What has happened lately.
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabPanel, VTabs } from 'vectis-ui'
const tab = ref('overview')
</script>
<template>
<VTabs v-model="tab" label="Project">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="activity">What has happened lately.</VTabPanel>
</template>
</VTabs>
</template>
Exemples
Variantes et tonalités
variant pose d'un mot le rail et le cadre : flat trace un filet sous la rangée et souligne l'onglet choisi, outlined met cette même barre et ses panneaux dans une carte, inset pose la rangée dans un rail creux. tone colore l'onglet choisi et rien d'autre, avec les trois valeurs qu'offre un bouton.
vue
flat
outlined
inset
accent
neutral
danger
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs, type TabsTone, type TabsVariant } from 'vectis-ui'
const variants: TabsVariant[] = ['flat', 'outlined', 'inset']
const tones: TabsTone[] = ['accent', 'neutral', 'danger']
const selectedByVariant = ref<Record<string, string>>({
flat: 'overview',
outlined: 'overview',
inset: 'overview',
})
const selectedByTone = ref<Record<string, string>>({
accent: 'overview',
neutral: 'overview',
danger: 'overview',
})
</script>
<template>
<div class="demo">
<!-- Two decisions in one word. `flat` draws a rule under the tabs and underlines
the selected one; `outlined` puts that same bar and its panels inside a card;
`inset` turns the row into a segmented control in a hollow track. -->
<div v-for="value in variants" :key="value" class="row">
<p class="caption">{{ value }}</p>
<VTabs v-model="selectedByVariant[value]" :variant="value" :label="`Project, ${value}`">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
</VTabs>
</div>
<!-- The tone colours the selected tab and nothing else: the others stay neutral
whatever it says, since only one of them is making a claim. -->
<div v-for="value in tones" :key="value" class="row">
<p class="caption">{{ value }}</p>
<VTabs v-model="selectedByTone[value]" :tone="value" :label="`Project, ${value}`">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
</VTabs>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-6);
}
.row {
display: grid;
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 la barre.
vue
xs
sm
md
lg
xl
md compact
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs, type TabsSize } from 'vectis-ui'
const sizes: TabsSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
const selected = ref<Record<string, string>>({
xs: 'overview',
sm: 'overview',
md: 'overview',
lg: 'overview',
xl: 'overview',
compact: 'overview',
})
</script>
<template>
<div class="demo">
<!-- The scale shared by every control, set once on the bar: each tab is a button
of that size, so a tab and a button beside it line up. -->
<div v-for="size in sizes" :key="size" class="row">
<p class="caption">{{ size }}</p>
<VTabs v-model="selected[size]" :size="size" :label="`Project, ${size}`">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
</VTabs>
</div>
<!-- compact takes 4px off, as everywhere else. -->
<div class="row">
<p class="caption">md compact</p>
<VTabs v-model="selected.compact" size="md" compact label="Project, compact">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
</VTabs>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: grid;
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 onglet
Un onglet accepte un libellé, une icône à chaque extrémité par iconStart et iconEnd, dessinée pleine sous iconFilled, ou le slot par défaut pour ce qu'une chaîne ne peut pas porter. Un onglet 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, VTab, VTabs } from 'vectis-ui'
import { description, notifications, schedule, search } from 'vectis-ui/icons'
const tab = ref('files')
</script>
<template>
<VTabs v-model="tab" label="Workspace">
<!-- A label, an icon at either end, or both. -->
<VTab value="files" label="Files" :icon-start="description" />
<VTab value="recent" label="Recent" :icon-start="schedule" />
<!-- An icon on its own still has to say what it is: with no label there is no
accessible name, so one is given here. -->
<VTab value="search" :icon-start="search" aria-label="Search" />
<!-- The default slot replaces the label, for anything a string cannot hold. -->
<VTab value="alerts">
<VBadge :count="3">Alerts</VBadge>
</VTab>
<VTab value="muted" label="Muted" :icon-end="notifications" />
</VTabs>
</template>
Panneaux
Un panneau masqué est masqué et non détruit : son contenu garde son état et un champ qu'il contient est toujours soumis. lazy retient le contenu jusqu'à la première ouverture. Sans le slot #panels, aucune zone de panneaux n'est rendue, et ce slot doit être présent ou absent dès le départ. Une fois présent, chaque onglet demande son panneau. Un panneau peut contenir un autre VTabs, qui garde sa propre mise en page.
vue
What the project is for.
Type here, switch tab and come back: the value is still there.
<script setup lang="ts">
import { ref } from 'vue'
import { VInput, VTab, VTabPanel, VTabs } from 'vectis-ui'
const withPanels = ref('overview')
const bare = ref('day')
</script>
<template>
<div class="demo">
<!-- A hidden panel is hidden and not destroyed: what it holds keeps its state, and
a field inside it is still submitted with the form. `lazy` is the exception,
and only for the first showing: it holds the content back until the panel is
opened once, then keeps it like the others. -->
<VTabs v-model="withPanels" label="Project">
<VTab value="overview" label="Overview" />
<VTab value="details" label="Details" />
<VTab value="history" label="History" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="details">
<VInput label="Reference" model-value="INV-2481" />
<p class="note">Type here, switch tab and come back: the value is still there.</p>
</VTabPanel>
<VTabPanel value="history" lazy>Built the first time this tab is opened.</VTabPanel>
</template>
</VTabs>
<!-- Leave the slot out and no panel area is rendered at all: the same component is
then a plain bar, or a segmented control switching a view that lives
somewhere else on the page. -->
<VTabs v-model="bare" variant="inset" label="Range">
<VTab value="day" label="Day" />
<VTab value="week" label="Week" />
<VTab value="month" label="Month" />
</VTabs>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-6);
}
.note {
margin: var(--vectis-space-3) 0 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Alignement
align dit où se placent les onglets le long de la barre quand ils ne la remplissent pas. Il se pose sur la barre et non sur la liste des onglets.
vue
start
center
end
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs, type TabsAlign } from 'vectis-ui'
const aligns: TabsAlign[] = ['start', 'center', 'end']
const selected = ref<Record<string, string>>({
start: 'overview',
center: 'overview',
end: 'overview',
})
</script>
<template>
<div class="demo">
<!-- Where the tabs sit when they do not fill the bar. It is set on the bar and not
on the list of tabs: pushing an overflowing list around would put whatever ran
past the start edge out of reach. -->
<div v-for="align in aligns" :key="align" class="row">
<p class="caption">{{ align }}</p>
<VTabs v-model="selected[align]" :align="align" :label="`Project, ${align}`">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
</VTabs>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: grid;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Remplir la barre
fullWidth partage toute la barre entre les onglets en parts égales, un libellé trop long pour sa part étant tronqué. Il est incompatible avec le défilement par construction.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs } from 'vectis-ui'
const tab = ref('day')
const wide = ref('overview')
</script>
<template>
<div class="demo">
<!-- The tabs share the whole bar between them, in equal parts whatever their
labels are worth. It is what turns a short row into a segmented control that
spans its container. -->
<VTabs v-model="tab" variant="inset" full-width label="Range">
<VTab value="day" label="Day" />
<VTab value="week" label="Week" />
<VTab value="month" label="Month" />
</VTabs>
<!-- A label too long for its share is truncated rather than allowed to widen it.
Filling the bar and scrolling are incompatible by construction: tabs told to fill the
bar can never overflow it. -->
<VTabs v-model="wide" full-width label="Project">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity and recent changes" />
<VTab value="settings" label="Settings" />
</VTabs>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-6);
max-inline-size: 28rem;
}
</style>
Orientation
orientation à vertical fait descendre les onglets sur le côté, les panneaux se plaçant à côté d'eux et les flèches suivant l'axe.
vue
What the project is for.
What has happened lately.
How it is configured.
What the project is for.
What has happened lately.
How it is configured.
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabPanel, VTabs } from 'vectis-ui'
const flat = ref('overview')
const framed = ref('overview')
</script>
<template>
<div class="demo">
<!-- Down the side instead of across: the panels then sit beside the tabs rather
than under them, and the arrow keys follow the axis. -->
<VTabs v-model="flat" orientation="vertical" label="Project">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="activity">What has happened lately.</VTabPanel>
<VTabPanel value="settings">How it is configured.</VTabPanel>
</template>
</VTabs>
<!-- Framed, the rule between the tabs and the panels moves to the other edge of
the column: the frame already draws the outer one, and what is missing is the
boundary between the two halves. -->
<VTabs v-model="framed" orientation="vertical" variant="outlined" label="Project, framed">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="activity">What has happened lately.</VTabPanel>
<VTabPanel value="settings">How it is configured.</VTabPanel>
</template>
</VTabs>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-6);
}
</style>
Défilement
Trop d'onglets pour la place et la barre défile, au doigt, au pavé tactile et au clavier. Le conteneur demande une taille minimale nulle, sans quoi la liste élargit son parent au lieu de déborder.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs } from 'vectis-ui'
const cities = ['Paris', 'Lisbon', 'Berlin', 'Madrid', 'Rome', 'Vienna', 'Prague', 'Oslo']
const tab = ref('Paris')
</script>
<template>
<!-- Too many tabs for the room, and the bar scrolls: by touch, by trackpad, and from
the keyboard, where the arrow keys bring the tab they reach back into view. The
scrollbar itself is hidden, the tabs running off the edge being the cue. -->
<div class="narrow">
<VTabs v-model="tab" label="Cities">
<VTab v-for="city in cities" :key="city" :value="city" :label="city" />
</VTabs>
</div>
</template>
<style scoped>
.narrow {
/* A grid or flex item does not shrink below its content unless told to, so without
this the list would simply widen its container instead of overflowing. */
min-inline-size: 0;
max-inline-size: 26rem;
}
</style>
Boutons de défilement
scrollButtons ajoute un bouton à chaque extrémité de la barre, chacun désactivé une fois cette extrémité atteinte. Un bouton qui avait le focus clavier le passe au bouton opposé en se désactivant. Il est optionnel, et exclut fullWidth.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs } from 'vectis-ui'
const cities = ['Paris', 'Lisbon', 'Berlin', 'Madrid', 'Rome', 'Vienna', 'Prague', 'Oslo']
const horizontal = ref('Paris')
const vertical = ref('Paris')
</script>
<template>
<div class="demo">
<!-- A button at each end, disabled once that end is reached, for a pointer that has
no wheel to scroll with. They are opt-in: on a bar that never overflows they
would be two permanently disabled controls. -->
<div class="narrow">
<VTabs v-model="horizontal" scroll-buttons label="Cities">
<VTab v-for="city in cities" :key="city" :value="city" :label="city" />
</VTabs>
</div>
<!-- Vertically the buttons move to the top and bottom of the column, and their
icons follow the axis without being asked. -->
<div class="short">
<VTabs v-model="vertical" orientation="vertical" scroll-buttons label="Cities, vertical">
<VTab v-for="city in cities" :key="city" :value="city" :label="city" />
</VTabs>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-6);
}
.narrow {
min-inline-size: 0;
max-inline-size: 26rem;
}
.short {
block-size: 14rem;
}
</style>
Flèches personnalisées
prevIcon et nextIcon remplacent les flèches, dont les valeurs par défaut suivent l'orientation. prevLabel et nextLabel sont ce que lit un lecteur d'écran pour ces deux boutons, et se rabattent sur le dictionnaire.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabs } from 'vectis-ui'
import { arrow_left_alt as arrowLeftAlt, arrow_right_alt as arrowRightAlt } from 'vectis-ui/icons'
const cities = ['Paris', 'Lisbon', 'Berlin', 'Madrid', 'Rome', 'Vienna', 'Prague', 'Oslo']
const tab = ref('Paris')
</script>
<template>
<!-- The default icons follow the orientation, chevrons across and carets down, so
there is nothing to set on either axis. Replace them when the surrounding design
asks for another glyph; the labels are what a screen reader reads, and they fall
back to the dictionary in the current language. -->
<div class="narrow">
<VTabs
v-model="tab"
scroll-buttons
:prev-icon="arrowLeftAlt"
:next-icon="arrowRightAlt"
prev-label="Earlier cities"
next-label="Later cities"
label="Cities"
>
<VTab v-for="city in cities" :key="city" :value="city" :label="city" />
</VTabs>
</div>
</template>
<style scoped>
.narrow {
min-inline-size: 0;
max-inline-size: 26rem;
}
</style>
Sélectionner à l'arrivée
activation est manuelle par défaut : une flèche déplace le focus, et Entrée ou Espace choisit. En automatique, l'onglet atteint par le focus est choisi, ce qui convient à un panneau qui apparaît instantanément.
vue
manual, the default
What the project is for.
What has happened lately.
How it is configured.
automatic
What the project is for.
What has happened lately.
How it is configured.
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabPanel, VTabs } from 'vectis-ui'
const manual = ref('overview')
const automatic = ref('overview')
</script>
<template>
<div class="demo">
<!-- Manual by default: an arrow key moves the focus and Enter or Space selects.
That is what a panel costing a request needs, since otherwise every tab passed
over on the way would fire one. -->
<div class="row">
<p class="caption">manual, the default</p>
<VTabs v-model="manual" label="Project, manual">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="activity">What has happened lately.</VTabPanel>
<VTabPanel value="settings">How it is configured.</VTabPanel>
</template>
</VTabs>
</div>
<!-- Selecting on arrival is what the ARIA authoring practices recommend when the
panel appears instantly: the reader hears the panel as they walk the row,
instead of having to confirm each one. Focus a tab and press an arrow key. -->
<div class="row">
<p class="caption">automatic</p>
<VTabs v-model="automatic" activation="automatic" label="Project, automatic">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" />
<VTab value="settings" label="Settings" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="activity">What has happened lately.</VTabPanel>
<VTabPanel value="settings">How it is configured.</VTabPanel>
</template>
</VTabs>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-6);
}
.row {
display: grid;
gap: var(--vectis-space-2);
}
.caption {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-caption-size);
}
</style>
Onglets désactivés
Un onglet disabled ne répond plus, se grise par les tokens de couleur et est enjambé par les flèches. Ne laissez pas la valeur pointer dessus, sinon la barre n'a plus aucun arrêt de tabulation. disabled posé sur VTabs éteint d'un coup tous les onglets et les boutons de défilement, le panneau affiché restant affiché.
vue
What the project is for.
Never reached.
How it is configured.
<script setup lang="ts">
import { ref } from 'vue'
import { VTab, VTabPanel, VTabs } from 'vectis-ui'
const tab = ref('overview')
</script>
<template>
<!-- A disabled tab is a disabled button: it stops responding, greys out through the
colour tokens rather than through an opacity, and the arrow keys step over it as
if it were not in the row. Its panel is simply never shown. -->
<VTabs v-model="tab" label="Project">
<VTab value="overview" label="Overview" />
<VTab value="activity" label="Activity" disabled />
<VTab value="settings" label="Settings" />
<template #panels>
<VTabPanel value="overview">What the project is for.</VTabPanel>
<VTabPanel value="activity">Never reached.</VTabPanel>
<VTabPanel value="settings">How it is configured.</VTabPanel>
</template>
</VTabs>
</template>
API
Props
VTabs
Prop
Type
Défaut
variant
TabsVariant'flat' | 'outlined' | 'inset'
'flat'
Comment la barre est encadrée. flat ne dessine qu'un filet sous les onglets, celui sélectionné étant souligné ; outlined place cette même barre et ses panneaux dans une carte ; inset transforme la rangée en contrôle segmenté posé dans une piste creuse.
tone
TabsTone'accent' | 'neutral' | 'danger'
'accent'
La couleur que prend l'onglet sélectionné. Les autres restent neutres quoi que dise cette prop.
size
TabsSize'xs' | 'sm' | 'md' | 'lg' | 'xl'
'md'
La hauteur des onglets, tirée de l'échelle partagée par tous les contrôles.
compact
boolean
false
Retire 4px à la hauteur de chaque onglet.
orientation
TabsOrientation'horizontal' | 'vertical'
'horizontal'
Si les onglets courent en travers de la page ou le long de son côté.
align
TabsAlign'start' | 'center' | 'end'
'start'
Où se placent les onglets le long de la barre quand ils ne la remplissent pas.
fullWidth
boolean
false
Fait partager toute la barre aux onglets, en parts égales.
scrollButtons
boolean
false
Ajoute un bouton à chaque extrémité de la barre pour la faire défiler, chacun désactivé une fois cette extrémité atteinte. Cela n'a de sens que si les onglets peuvent déborder, donc c'est exclusif de fullWidth.
L'icône du bouton de défilement avant. Elle suit l'orientation par défaut.
prevLabel
string
aucune
Ce que fait le bouton de défilement arrière, en mots. Il retombe sur le dictionnaire.
nextLabel
string
aucune
Ce que fait le bouton de défilement avant, en mots. Il retombe sur le dictionnaire.
activation
TabsActivation'manual' | 'automatic'
'manual'
Si se déplacer sur un onglet le sélectionne aussi. Sélectionner à l'arrivée est ce que recommandent les pratiques ARIA quand un panneau apparaît instantanément ; laissez en manuel quand afficher un panneau coûte une requête, sinon chaque onglet survolé en déclencherait une.
disabled
boolean
false
Rend tous les onglets inutilisables, et les boutons de défilement avec eux : les onglets quittent l'ordre de tabulation et se grisent par les tokens de couleur. Un onglet désactivé de lui-même le reste dans tous les cas. Le panneau affiché reste affiché.
label
string
aucune
Ce que les lecteurs d'écran annoncent pour la rangée d'onglets. Il retombe sur le dictionnaire du design system.
La valeur de l'onglet sélectionné. Il n'y a délibérément aucune valeur par défaut : le composant ne peut pas savoir lequel des onglets que vous avez écrits doit s'ouvrir. Elle doit désigner un onglet qui existe et n'est pas désactivé, faute de quoi aucun onglet n'a d'arrêt de tabulation et la barre devient inatteignable au clavier.
Le nom de cet onglet dans le code. Le panneau portant la même valeur est celui qu'il montre, et c'est aussi ce que porte la valeur quand cet onglet est sélectionné.
label
string
aucune
Le libellé visible. Le slot par défaut le remplace.
Une icône après le libellé, pour un compteur ou un état que l'onglet porte.
iconFilled
boolean
false
Rend iconStart et iconEnd dans leur forme pleine, l'axe FILL de la police.
disabled
boolean
false
Rend l'onglet inutilisable : il ne répond plus, les flèches l'enjambent, et il se grise par les tokens de couleur. Un VTabs posé disabled désactive tous les onglets, celui-ci compris, quoi que dise cette prop.
Quel onglet montre ce panneau : ce doit être la valeur de l'un d'eux.
lazy
boolean
false
Retient le contenu jusqu'au premier affichage du panneau, et le conserve ensuite. C'est fait pour un panneau coûteux à construire ; l'état qu'il porte reste préservé par la suite.
Slots
VTabs
Slot
Type
default
{}
Les onglets eux-mêmes.
panels
{}
Les panneaux que montrent les onglets. L'omettre ne rend aucune zone de panneau, ce qui est la façon dont le même composant sert de simple barre ou de contrôle segmenté.
VTab
Slot
Type
default
{}
Le contenu de l'onglet, 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.
VTabPanel
Slot
Type
default
{}
Ce que contient le panneau.
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.