Raccourci clavier : Ctrl + K
Commencer

Carrousel

Des diapositives parcourues au doigt, au pavé tactile, à la barre de défilement ou au clavier. C'est un seul conteneur natif à accroche de défilement : rien n'est cloné, et le nombre de diapositives qui tiennent est décidé par le CSS sans un seul point de rupture.

Utilisation

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

const slide = ref(0)
</script>

<template>
  <VCarousel v-model="slide" label="Gallery">
    <VCarouselItem><p class="slide">First slide</p></VCarouselItem>
    <VCarouselItem><p class="slide">Second slide</p></VCarouselItem>
    <VCarouselItem><p class="slide">Third slide</p></VCarouselItem>
  </VCarousel>
</template>

<style scoped>
/* A slide has no height of its own: it takes the one its content brings. */
.slide {
  display: grid;
  place-items: center;
  block-size: 12rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  background: var(--vectis-color-surface-muted);
}
</style>

Exemples

Diapositives par vue

itemsPerView est le nombre de diapositives visibles à la fois, et itemMinSize la taille minimale de chacune avant qu'il en tienne moins.

vue
<script setup lang="ts">
import { VCarousel, VCarouselItem } from 'vectis-ui'

/* Flat colours rather than pictures, so one slide is told from the next at a glance. */
const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <VCarousel label="Three at a time" :items-per-view="3" :item-min-size="180">
    <VCarouselItem v-for="(hue, i) in hues" :key="hue">
      <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
    </VCarouselItem>
  </VCarousel>
</template>

<style scoped>
/* A slide has no height of its own: it takes the one its content brings. */
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

Débord

peek laisse dépasser une bande de la diapositive suivante, écart compris.

vue
<script setup lang="ts">
import { VCarousel, VCarouselItem } from 'vectis-ui'

const products = ['Kettle', 'Toaster', 'Blender', 'Grinder', 'Scale', 'Press']
const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <VCarousel label="Product list" :items-per-view="2" peek="3rem">
    <VCarouselItem v-for="(product, i) in products" :key="product">
      <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hues[i]})` }">{{ product }}</p>
    </VCarouselItem>
  </VCarousel>
</template>

<style scoped>
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

Effets

effect décide comment une diapositive cède la place à la suivante : slide n'anime rien, fade fait fondre chaque diapositive sur place et demande une seule diapositive à la fois sans peek, scale recule les voisines.

vue
slide
fade
scale
<script setup lang="ts">
import { VCarousel, VCarouselItem, VTypography, type CarouselEffect } from 'vectis-ui'

const effects: CarouselEffect[] = ['slide', 'fade', 'scale']
const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <div v-for="effect in effects" :key="effect" class="group">
    <VTypography variant="overline" tone="muted">{{ effect }}</VTypography>
    <VCarousel :effect="effect" :label="`The ${effect} effect`">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>
</template>

<style scoped>
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

Orientation

orientation à vertical fait pivoter tout le composant sur l'axe de bloc. Donnez aussi une height, dont les diapositives prennent une part.

vue
horizontal
vertical
<script setup lang="ts">
import { VCarousel, VCarouselItem, VTypography } from 'vectis-ui'

const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <div class="group">
    <VTypography variant="overline" tone="muted">horizontal</VTypography>
    <VCarousel label="Horizontal gallery">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>

  <div class="group">
    <VTypography variant="overline" tone="muted">vertical</VTypography>
    <!-- Scrolling downwards, the height is the reference a slide takes its share of. -->
    <VCarousel orientation="vertical" height="14rem" label="Vertical gallery">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide fill" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>
</template>

<style scoped>
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
/* The vertical slide is already sized by the carousel: the content fills it rather than
   bringing a height of its own. */
.fill {
  block-size: 100%;
}
</style>

Icônes personnalisées

prevIcon et nextIcon acceptent un IconSource, et prevLabel et nextLabel les mots annoncés par ces boutons.

vue
chevrons
carets
<script setup lang="ts">
import { VCarousel, VCarouselItem, VTypography } from 'vectis-ui'
import {
  chevron_left as chevronLeft,
  chevron_right as chevronRight,
  expand_less as expandLess,
  expand_more as expandMore,
} from 'vectis-ui/icons'

const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <div class="group">
    <VTypography variant="overline" tone="muted">chevrons</VTypography>
    <VCarousel label="Chevron controls" :prev-icon="chevronLeft" :next-icon="chevronRight">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>

  <div class="group">
    <VTypography variant="overline" tone="muted">carets</VTypography>
    <VCarousel
      orientation="vertical"
      height="14rem"
      label="Caret controls"
      :prev-icon="expandLess"
      :next-icon="expandMore"
    >
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide fill" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>
</template>

<style scoped>
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
.fill {
  block-size: 100%;
}
</style>

Placements

controls et indicators se placent indépendamment : inside les pose sur les diapositives, outside à côté, false les retire.

vue
arrows inside, dots outside
arrows inside, dots inside
arrows outside, dots outside
arrows outside, dots inside
<script setup lang="ts">
import {
  VCarousel,
  VCarouselItem,
  VTypography,
  type CarouselControls,
  type CarouselIndicators,
} from 'vectis-ui'

const placements: {
  caption: string
  controls: CarouselControls
  indicators: CarouselIndicators
}[] = [
  { caption: 'arrows inside, dots outside', controls: 'inside', indicators: 'outside' },
  { caption: 'arrows inside, dots inside', controls: 'inside', indicators: 'inside' },
  { caption: 'arrows outside, dots outside', controls: 'outside', indicators: 'outside' },
  { caption: 'arrows outside, dots inside', controls: 'outside', indicators: 'inside' },
]

const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <div v-for="placement in placements" :key="placement.caption" class="group">
    <VTypography variant="overline" tone="muted">{{ placement.caption }}</VTypography>
    <VCarousel
      :controls="placement.controls"
      :indicators="placement.indicators"
      :label="placement.caption"
    >
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>
</template>

<style scoped>
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

Sauts

Un déplacement de plus d'une page se fait d'un coup, l'effet étant joué une fois à l'arrivée. noJump rétablit le trajet complet, sur tous les chemins.

vue
straight there, the default
the whole travel
<script setup lang="ts">
import { VCarousel, VCarouselItem, VTypography } from 'vectis-ui'

const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <div class="group">
    <VTypography variant="overline" tone="muted">straight there, the default</VTypography>
    <VCarousel effect="fade" label="Jumping gallery">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>

  <div class="group">
    <VTypography variant="overline" tone="muted">the whole travel</VTypography>
    <VCarousel effect="fade" no-jump label="Travelling gallery">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>
</template>

<style scoped>
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
.slide {
  display: grid;
  place-items: center;
  block-size: 10rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

Boucle

loop ramène la dernière position vers la première, si bien qu'aucun bouton n'est jamais désactivé. Cela vaut pour les boutons, les flèches du clavier et la lecture automatique.

vue
<script setup lang="ts">
import { VCarousel, VCarouselItem } from 'vectis-ui'

const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <VCarousel loop label="Looping gallery">
    <VCarouselItem v-for="(hue, i) in hues" :key="hue">
      <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
    </VCarouselItem>
  </VCarousel>
</template>

<style scoped>
.slide {
  display: grid;
  place-items: center;
  block-size: 12rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

Défilement automatique

autoplay est un intervalle en millisecondes, zéro le désactivant. Il se suspend au survol et au focus clavier, et ne tourne jamais pour un lecteur qui a demandé moins d'animation. Aucun bouton de pause n'est rendu : ajoutez-en un, comme le fait l'exemple.

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

/* The stop control the component leaves to you: the prop is reactive, so 0 cancels the
   timer on the spot. Hover and keyboard focus already hold the rotation, which leaves a
   touch user with nothing without this button. */
const paused = ref(false)

const hues = [220, 280, 340, 20, 90, 160]
</script>

<template>
  <div class="group">
    <VButton variant="outline" tone="neutral" size="sm" class="stop" @click="paused = !paused">
      {{ paused ? 'Resume' : 'Pause' }}
    </VButton>

    <VCarousel :autoplay="paused ? 0 : 3000" label="Rotating gallery">
      <VCarouselItem v-for="(hue, i) in hues" :key="hue">
        <p class="slide" :style="{ background: `oklch(0.45 0.15 ${hue})` }">{{ i + 1 }}</p>
      </VCarouselItem>
    </VCarousel>
  </div>
</template>

<style scoped>
.group {
  display: grid;
  gap: var(--vectis-space-3);
}
.stop {
  justify-self: start;
}
.slide {
  display: grid;
  place-items: center;
  block-size: 12rem;
  margin: 0;
  border-radius: var(--vectis-radius-surface);
  color: var(--vectis-color-text-on-accent);
  font-size: var(--vectis-text-heading-3-size);
  font-weight: var(--vectis-text-heading-3-weight);
}
</style>

API

Props

VCarousel
PropTypeDéfaut
itemsPerViewnumber1
Combien de diapositives peuvent être visibles à la fois. C'est un MAXIMUM et non une cible : le plancher ci-dessous décide combien tiennent réellement, ce qui rend l'ensemble adaptatif sans point de rupture.
itemMinSizenumber | stringaucune
Jusqu'où une diapositive peut rétrécir. Dès qu'une part égale passerait sous cette valeur, moins de diapositives tiennent et le carrousel défile plus loin à la place. Un nombre est lu en pixels ; tout le reste est utilisé tel quel, donc '20vw' fonctionne.
peeknumber | stringaucune
Quelle part de la diapositive SUIVANTE reste visible, pour indiquer qu'il y a une suite. Elle inclut l'écart qui la précède. Elle ne peut pas se combiner à l'effet de fondu, qui suppose qu'une diapositive remplit exactement la vue.
gapnumber | stringaucune
L'espace entre deux diapositives.
orientationCarouselOrientation'horizontal' | 'vertical''horizontal'
Si le carrousel défile en travers de la page ou de haut en bas.
effectCarouselEffect'slide' | 'fade' | 'scale''slide'
Comment une diapositive cède la place à la suivante, piloté par le défilement lui-même. Le glissement ne signifie aucune animation. Le fondu exige UNE diapositive à la fois et aucun débord, puisqu'il maintient chaque diapositive en place pendant que le défilement passe dessous ; demandé autrement, il retombe sur le glissement plutôt que de se dégrader.
heightnumber | stringaucune
La hauteur de la zone visible. DONNEZ-EN UNE quand le carrousel défile vers le bas : une diapositive dimensionnée en part de la hauteur a besoin d'une hauteur DONT prendre une part, et sans elle chaque diapositive s'effondre sur son propre contenu. En défilement horizontal, la hauteur vient des diapositives elles-mêmes.
loopbooleanfalse
Si le carrousel revient au début : après la dernière position il retourne à la première, et avant la première il va à la dernière. Rien n'est cloné pour cela : la vraie piste revient au début, d'un coup, en jouant la transition à l'arrivée plutôt qu'en passant devant chaque diapositive intermédiaire. Sans effet là où il n'y a qu'une seule position de repos, et les boutons y restent désactivés plutôt que de devenir deux contrôles qui ne font rien.
noJumpbooleanfalse
Si un déplacement de plus d'une page conserve tout le défilement au lieu d'aller directement à destination. Désactivé par défaut : un point situé cinq pages plus loin arrive d'un coup et joue la transition une fois, à l'arrivée. Activez-le quand le trajet est le sujet, sur une poignée de diapositives où voir la piste défiler dit quelque chose de la distance parcourue. Il couvre tous les chemins, les points, les touches Origine et Fin et un carrousel en boucle qui revient au début, et il ne change rien pour un lecteur ayant demandé moins de mouvement, cette préférence rendant déjà tout défilement instantané.
autoplaynumber0
Combien de temps chaque diapositive est montrée avant la suivante, en millisecondes ; zéro signifie qu'il n'avance pas de lui-même. Il s'arrête à la dernière page sauf si le carrousel boucle, se met en pause tant que le pointeur y repose ou que le focus CLAVIER est à l'intérieur, et ne tourne jamais pour un lecteur ayant demandé moins de mouvement. Aucun bouton de pause n'est rendu : cette prop est réactive, donc la lier à zéro est un contrôle d'arrêt d'une ligne de votre côté, et il vaut la peine de l'ajouter, puisque le survol et le focus ne laissent rien à un utilisateur tactile. La boucle rend cette liaison nécessaire plutôt que recommandée, le mouvement ne s'arrêtant plus de lui-même.
controlsCarouselControlsfalse | 'inside' | 'outside''inside'
Où vont les boutons précédent et suivant : par-dessus les diapositives, à côté, ou nulle part. Placés à côté, leur place est réservée en rembourrage, si bien que l'encombrement du composant est inchangé et que ce sont les diapositives qui rétrécissent. Dans les deux cas ils sont centrés sur les DIAPOSITIVES et jamais sur les diapositives plus les points.
indicatorsCarouselIndicatorsfalse | 'inside' | 'outside''outside'
Où vont les points de position : par-dessus les diapositives, après elles, ou nulle part. Après elles signifie en dessous quand le carrousel défile horizontalement, et à côté quand il défile verticalement.
controlsVisibilityCarouselControlsVisibility'always' | 'hover''always'
Si ces boutons sont toujours visibles, ou n'apparaissent que quand le pointeur est sur le carrousel ou que le focus clavier est à l'intérieur. Là où il n'y a pas de pointeur pour survoler, ils restent visibles quoi que dise cette prop. Les points ne sont jamais masqués.
prevIconIconSourceaucune
L'icône du bouton précédent. Elle suit l'orientation par défaut.
nextIconIconSourceaucune
L'icône du bouton suivant. Elle suit l'orientation par défaut.
prevLabelstringaucune
Ce que fait le bouton précédent, en mots. Il retombe sur le dictionnaire.
nextLabelstringaucune
Ce que fait le bouton suivant, en mots. Il retombe sur le dictionnaire.
labelstringaucune
Ce que les lecteurs d'écran annoncent pour le carrousel dans son ensemble. Donnez-en un DISTINCT à chaque carrousel d'une page : c'est un point de repère, et deux points de repère portant le même nom sont indiscernables pour qui navigue entre eux.
v-modelnumber0
Quelle diapositive est courante : la première entièrement visible quand plusieurs tiennent à la fois, ce qui est aussi la position où le carrousel s'est arrêté. Une valeur hors des positions où le carrousel peut s'arrêter y est ramenée.
VCarouselItem
PropTypeDéfaut
indexnumber0
Quelle diapositive est celle-ci parmi ses voisines. Le carrousel l'injecte en les rendant. Ne la passez JAMAIS à la main : c'est ce qui rend le « 3 sur 8 » annoncé par un lecteur d'écran identique sur le serveur et dans le navigateur.

Slots

VCarousel
SlotType
default{}
Les diapositives. Leur nombre est lu depuis ce que ce slot REND, donc un v-for convient parfaitement, mais le slot ne doit pas dépendre de quelque chose de vrai seulement dans un navigateur, sans quoi le serveur et le client compteraient différemment.
controlsCarouselControlsSlotProps
Remplace entièrement les boutons précédent et suivant, leur placement compris : un contenu personnalisé se positionne donc lui-même, et le réglage de visibilité ne s'y applique plus.
indicatorsCarouselIndicatorsSlotProps
Remplace toute la barre de points. Rendez un contrôle par POSITION et non par diapositive : une position au-delà de la dernière ne peut pas être atteinte, donc une barre bâtie sur le nombre de diapositives propose des points qui ne mènent nulle part. Le nombre de diapositives est passé aussi, pour une formulation comme « 3 sur 8 ».
indicatorCarouselIndicatorSlotProps
Remplace ce qui est dessiné À L'INTÉRIEUR d'un point. Le bouton lui-même, et tout ce qui le fait annoncer et se comporter correctement, reste celui du design system.
VCarouselItem
SlotType
default{}
Le contenu de la diapositive : une image, une carte, du texte libre.

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 interface CarouselControlsSlotProps {
  previous: () => void
  next: () => void
  atStart: boolean
  atEnd: boolean
  index: number
  count: number
  pageCount: number
  orientation: CarouselOrientation
}
export interface CarouselIndicatorSlotProps {
  index: number
  active: boolean
}
export interface CarouselIndicatorsSlotProps {
  index: number
  count: number
  pageCount: number
  goTo: (index: number) => void
  orientation: CarouselOrientation
}
export type CarouselOrientation = 'horizontal' | 'vertical'
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

Variables CSS

TokenValeur
--vectis-control-size-carousel-block24rem
--vectis-control-size-carousel-indicator0.625rem
--vectis-control-size-carousel-indicator-active1.25rem