Carousel
BêtaComposantUn carrousel de cartes, de photos ou de slides plein écran, que l'on glisse, lance ou fait défiler, avec une physique fidèle au doigt.
- Frameworks
- ReactVueVanilla
- Technologies
- CSS
- Déclenché par
- ContinuGestePiloté par l'application
- Mis à jour le
- Construit sur
- FrameLoopPointerTracker
Carousel présente une rangée de contenus que l'on parcourt à son rythme : au doigt, à la souris, au trackpad, au clavier ou avec ses contrôles. Le contenu suit le geste au pixel près, un lancer garde son élan, et chaque slide se pose sur un ressort sans à-coup. Utilisez-le pour une rangée de produits, une galerie, des témoignages ou un hero. Pour des logos qui défilent seuls, sans que l'on ait à agir, préférez Marquee.
Installation
Installez l'élément et ses dépendances en une commande :
pnpm dlx shadcn@latest add https://o-ui.hd987.dev/r/carousel.jsonCarousel n'a aucune dépendance. Il installe avec lui les primitives FrameLoop, la boucle d'animation partagée par les animations d'O-UI, et PointerTracker, qui suit le pointeur pour toute la page. Les prérequis communs à tous les composants (Tailwind CSS v4, React 19 ou Vue 3.5) sont décrits dans Installation.
Utilisation
Carousel se compose de parties, comme le carrousel de shadcn : la racine, la zone visible et ses slides, puis les contrôles que vous placez où vous voulez.
import {
Carousel,
CarouselContent,
CarouselDots,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/o-ui/carousel/carousel";
export function Places() {
return (
<Carousel aria-label="Places to stay" className="w-full max-w-2xl">
<CarouselContent className="rounded-xl">
<CarouselItem>
<img
src="/places/ridge.webp"
alt="Alpine ridge above the clouds"
className="aspect-video w-full rounded-xl object-cover"
/>
</CarouselItem>
<CarouselItem>
<img
src="/places/coast.webp"
alt="Coastal cliffs over turquoise water"
className="aspect-video w-full rounded-xl object-cover"
/>
</CarouselItem>
<CarouselItem>
<img
src="/places/lake.webp"
alt="Misty lake at sunrise"
className="aspect-video w-full rounded-xl object-cover"
/>
</CarouselItem>
</CarouselContent>
<div className="mt-4 flex items-center justify-center gap-2">
<CarouselPrevious />
<CarouselDots />
<CarouselNext />
</div>
</Carousel>
);
}Quelques règles font la différence :
- Nommez le carrousel avec
aria-label, ouaria-labelledbyvers son titre : c'est une région de la page, et un lecteur d'écran l'annonce par ce nom. - Réglez la taille des slides sur la racine.
perView(1 par défaut, décimales acceptées) etgap(16 pixels) donnent la valeur de base. Des classes l'adaptent à la largeur :md:[--carousel-per-view:3],lg:[--carousel-gap:24px]. Pour une largeur propre à une slide, donnez-luibasis-autoavec sa largeur (basis-auto w-72) :basis-autoannule la part calculée par le carrousel, puis le carrousel mesure ce qui est rendu. - Placez les contrôles où vous voulez, tant qu'ils sont dans
Carousel: au-dessus, en dessous, ou par-dessus la zone visible en position absolue (la racine est enrelative). - Laissez de la place aux anneaux de focus. La zone visible coupe ce qui dépasse d'elle. Donnez-lui une marge intérieure compensée,
-m-1 p-1: les slides restent visibles dans cette marge, et l'anneau d'une carte n'est plus coupé. - Dans une grille ou une rangée flex, donnez
min-w-0au carrousel. Sinon la piste, plus large que la zone visible, élargit sa colonne. - En vertical, donnez une hauteur à
CarouselContent(h-96). Chaque slide en prend sa part selonperView. - Donnez aux images leurs dimensions (
width,heightou un ratio) : rien ne bouge pendant leur chargement. Le chargement différé (loading="lazy") convient : le carrousel charge en avance les images des slides voisines. Inutile d'ajouterdraggable="false": le carrousel empêche déjà le glisser natif des images et des liens.
Exemples
Rangée de produits
Des cartes cliquables : 1,3 carte sur un téléphone, 2,5 sur une tablette et 4 sur un ordinateur, grâce à perView={1.3} et aux classes sm:[--carousel-per-view:2.5] lg:[--carousel-per-view:4]. Les flèches de l'en-tête et un lancer avancent d'une page : autant de cartes qu'il en tient entièrement. CarouselProgress, décoratif, montre la part visible de la rangée, comme un ascenseur. Un glisser ne déclenche jamais le lien de la carte qu'il lâche.
Rangée de filtres
Des pastilles de catégories, chacune à la largeur de son libellé grâce à basis-auto sur CarouselItem. Avec snap={false} et swipe="free", la rangée glisse librement et s'arrête où son élan s'épuise. Les flèches, de part et d'autre, l'avancent d'une page, sans avoir à la glisser ; au clavier, la tabulation amène chaque pastille dans la vue. La zone visible porte min-w-0 flex-1 pour tenir entre les flèches.
Galerie avec miniatures
La fiche d'une annonce : la grande image, ses flèches et son compteur posés dessus, puis CarouselThumbnails. La n-ième miniature correspond à la n-ième photo : donnez-en une par slide, dans le même ordre. La miniature active reste visible, la bande se glisse librement, et un clic ou la touche Entrée change de photo. Les contrôles posés sur l'image reçoivent un fond translucide (bg-background/80 backdrop-blur-sm), lisible dans les deux thèmes.
Témoignages centrés
align="center" place la carte active au centre, effect="scale" met les voisines en retrait, et loop fait tourner la rangée sans fin. perView={1.3}, puis 2,2 et 2,8 cartes selon la largeur, laisse voir les voisines ; step="slide" fait avancer d'une carte à la fois, avec un point par carte. Avec selectOnClick, un clic sur une voisine l'amène au centre. La carte active change d'aspect grâce à l'attribut data-active que le carrousel pose sur sa slide (group-data-active/slide:bg-card, avec group/slide sur CarouselItem).
Hero avec autoplay
Un fondu enchaîné plein cadre (effect="fade") qui avance seul toutes les six secondes (autoplay, interval={6}), et repart de la première slide après la dernière (loop). Le bouton de rotation, placé en premier, et les points passent en blanc par leur className. Le texte de chaque slide monte et s'efface avec le fondu, en CSS seulement, grâce à --carousel-slide-distance.
Récit éditorial
Une liste de chapitres et le carrousel partagent un même état : index et onIndexChange (v-model:index en Vue). Choisir un chapitre fait glisser le carrousel, et glisser le carrousel met à jour la liste. Chaque photo dérive légèrement pendant le mouvement : une parallaxe écrite en une classe, avec --carousel-slide-progress. Le carrousel, dans une grille, porte min-w-0.
Personnalisation
Le mouvement. duration règle la durée visuelle d'un déplacement, en secondes (0,5 par défaut) ; bounce ajoute un rebond à l'arrivée, de 0 à 0,5. Un déplacement reprend la vitesse en cours : des clics rapprochés sur une flèche s'enchaînent sans saccade. Pour un carrousel vif, essayez duration={0.35} ; pour un hero posé, duration={0.8}.
Le lancer. swipe choisit jusqu'où l'élan d'un geste emporte le contenu :
"page"(par défaut) : une longueur visible au plus depuis la slide de départ. Une slide à la fois avec des slides pleine largeur, quelques cartes avec de plus petites ;"free": aussi loin que l'élan, pour une longue rangée que l'on survole ;"single": exactement une étape par geste, quelle que soit sa force.
Avec snap={false}, le contenu ne se pose plus sur une étape : il glisse librement et s'arrête où son élan s'épuise, comme une rangée de filtres ou de catégories.
Les étapes. step="page" (par défaut) fait avancer les flèches, les points, le clavier et l'autoplay d'une page : toutes les slides qui tiennent entièrement. Le compte se refait à chaque largeur d'écran. step="slide" avance d'une slide à la fois. containScroll (activé par défaut) évite le vide aux extrémités ; désactivé avec align="center", la première et la dernière slide peuvent se poser au centre.
Les effets. Pendant le mouvement, le carrousel écrit des variables CSS que vos classes peuvent lire :
| Variable | Où | Valeur |
|---|---|---|
--carousel-slide-progress | chaque slide | De −1 à 1 : 0 à sa place, −1 une slide avant, 1 une slide après |
--carousel-slide-distance | chaque slide | De 0 à 1 : l'éloignement de sa place |
--carousel-progress | racine | De 0 à 1 : la position dans le contenu |
--carousel-visible | racine | De 0 à 1 : la part du contenu visible |
Les deux variables de la racine sont écrites pour CarouselProgress. Pour les lire dans vos propres classes, posez data-carousel-progress sur un élément du carrousel. Recettes :
- Des voisines atténuées :
opacity-[calc(1-var(--carousel-slide-distance,0)*0.4)]surCarouselItem. Sur du texte, ne descendez pas sous 0,6. - Une légende qui monte avec sa slide :
[opacity:calc(1-var(--carousel-slide-distance,0)*2)] [translate:0_calc(var(--carousel-slide-distance,0)*1.5rem)]. - Une photo qui dérive : agrandissez-la dans un parent qui coupe ce qui dépasse.
<div className="overflow-hidden rounded-2xl">
<img
className="[translate:calc(var(--carousel-slide-progress,0)*-10%)_0] scale-125"
src="…"
alt="…"
/>
</div>Les valeurs de repli (var(…, 0)) gardent le contenu à sa place avant l'hydratation et sans JavaScript.
Les états. Le carrousel pose des attributs utilisables avec les variantes de Tailwind :
- sur la slide :
data-active(sélectionnée) etdata-in-view(visible à plus de moitié) ; - sur la racine :
data-dragging,data-can-prev,data-can-next,data-loopetdata-reduced-motion.
Par exemple, cursor-grab data-dragging:cursor-grabbing sur la racine montre une main au glisser, et group-data-active/slide:… change une carte active (donnez group/slide à CarouselItem).
Les contrôles. Chaque partie accepte className (class en Vue). Les points, le compteur, la barre et l'anneau du bouton de rotation prennent la couleur posée sur la partie elle-même : <CarouselDots className="text-white" /> les adapte à une photo. Les flèches et le bouton de rotation acceptent une autre icône en enfant (slot par défaut en Vue). Pour un indicateur de délai à vous, posez data-carousel-indicator="bar" sur un élément du carrousel : il grandit de scaleX(0) à scaleX(1) pendant chaque délai de l'autoplay. Donnez-lui son état de départ et son origine, comme les parties fournies : origin-left rtl:origin-right [transform:scaleX(0)].
Intégration
Next.js et Nuxt
Le rendu serveur produit déjà la mise en page finale : les slides ont leur taille, sans saut au chargement. Avant l'hydratation, ou sans JavaScript, la zone visible défile nativement et s'aimante sur chaque slide : le carrousel reste utilisable. À son démarrage, il reprend la position atteinte, puis prend le relais.
- Next.js : Carousel et ses parties sont des composants client, utilisables dans un Server Component avec des props simples. Des fonctions (
onIndexChange,onSettle, deslabelsavecslideLabel…) ne peuvent pas venir d'un Server Component : placez alors le carrousel dans un composant à vous, marqué"use client". UnLinkde Next.js fonctionne dans une slide, et un glisser ne le déclenche pas.next/imageaussi : avecfill, donnez àCarouselItemune position et une hauteur (relative aspect-video). - Nuxt : aucun
<ClientOnly>n'est nécessaire. Importez les parties explicitement, comme dans les exemples : l'import automatique de Nuxt préfixe le nom d'un composant par son dossier.
Les slides peuvent changer après le montage (une liste chargée, un filtre) : le carrousel les retrouve seul et garde la slide sélectionnée si elle existe encore.
Contrôler le carrousel depuis l'application
Un index contrôlé. index et onIndexChange (v-model:index en Vue) lient la slide sélectionnée à votre état :
indexdésigne la slide, à partir de 0. Quand il change, le carrousel s'y rend avec son mouvement habituel ; un index trop grand est ramené à la dernière slide.indexne fige pas le carrousel : la personne le déplace toujours.onIndexChangesignale chaque nouvelle slide, quelle qu'en soit la cause : un geste, un contrôle, l'autoplay, ou votre propreindex(avec cette même valeur).- Mettez votre état à jour dans
onIndexChange. Sinon il garde l'ancienne valeur, et la redonner plus tard ne déplacerait pas le carrousel. Aucune boucle n'est possible :onIndexChangen'est appelé que pour une slide nouvelle. onSettle(settleen Vue) signale l'arrêt.startIndexne choisit que la slide de départ ;indexl'emporte s'il est donné.
"use client";
import { useState } from "react";
import { Carousel } from "@/components/o-ui/carousel/carousel";
export function Chapters() {
const [slide, setSlide] = useState(0);
return (
<Carousel aria-label="Chapters" index={slide} onIndexChange={setSlide}>
…
</Carousel>
);
}Des contrôles sur mesure. Dans Carousel, useCarousel() donne l'état (index, count, snap, snapCount, canPrev, canNext, playing, autoplay, disabled) et les actions (next, prev, goTo, goToSnap, play, stop). L'état ne change qu'aux moments utiles, jamais à chaque frame : votre composant ne se rend pas pendant le mouvement. Passez { user: true } à une action déclenchée par la personne : le mouvement est annoncé aux lecteurs d'écran et l'autoplay s'arrête, comme avec les contrôles fournis. { instant: true } arrive sans animation.
"use client";
import { Carousel, CarouselContent, useCarousel } from "@/components/o-ui/carousel/carousel";
function Pager() {
const { snap, snapCount, canNext, next } = useCarousel();
return (
<button type="button" disabled={!canNext} onClick={() => next({ user: true })}>
Next ({snap + 1} of {snapCount})
</button>
);
}
export function Gallery() {
// useCarousel reads the enclosing Carousel: the pager is rendered inside it.
return (
<Carousel aria-label="Gallery">
<CarouselContent>…</CarouselContent>
<Pager />
</Carousel>
);
}Depuis l'extérieur. En React, actionsRef reçoit les mêmes actions ; la ref reste l'élément racine. En Vue, la ref posée sur Carousel expose el et ces actions.
"use client";
import { useRef } from "react";
import { Carousel, type CarouselActions } from "@/components/o-ui/carousel/carousel";
export function Products() {
const carousel = useRef<CarouselActions>(null);
return (
<>
<button type="button" onClick={() => carousel.current?.goTo(0, { user: true })}>
Back to the first product
</button>
<Carousel aria-label="Products" actionsRef={carousel}>
…
</Carousel>
</>
);
}Un clic sur CarouselPrevious, CarouselNext ou CarouselAutoplayToggle appelle d'abord votre onClick (@click en Vue) : event.preventDefault() y annule le pas ou la bascule.
Autoplay et pause
Avec autoplay, le carrousel avance d'une étape toutes les interval secondes (5 par défaut). Hors boucle, il revient au début après la dernière. Le délai repart de zéro à chaque changement de slide.
- Il se met en pause, puis reprend : au survol de la souris, pendant un glisser ou un défilement au trackpad, hors de l'écran, dans un onglet masqué, avec
paused, et avec la pause commune de la page. - Il s'arrête pour de bon quand la personne prend la main : focus clavier dans le carrousel, flèche, point ou miniature, et
CarouselAutoplayToggle. Il ne reprend que par ce bouton. - En mouvement réduit, il attend : il ne démarre pas seul, et le bouton propose de le lancer. Lancé par la personne, il avance en fondus.
La pause commune de FrameLoop arrête toutes les animations d'O-UI de la page, l'autoplay compris : un seul interrupteur suffit pour la WCAG 2.2.2.
import { setAnimationsPaused } from "@/components/o-ui/frame-loop/frame-loop-core";
// From a "Pause animations" switch of the page:
setAnimationsPaused(true);La recette complète de cet interrupteur, qui garde le choix du visiteur, est sur la page de FrameLoop.
Traduire les textes
Les textes lus par les lecteurs d'écran sont en anglais par défaut. labels en remplace tout ou partie. Définissez l'objet une fois, hors du composant : un objet recréé à chaque rendu fait relire ses textes au carrousel à chaque rendu.
"use client";
import { Carousel, type CarouselLabels } from "@/components/o-ui/carousel/carousel";
const LABELS: Partial<CarouselLabels> = {
carousel: "carrousel",
slide: "diapositive",
slides: "Diapositives",
slideLabel: (index, count) => `${index + 1} sur ${count}`,
previous: "Diapositive précédente",
next: "Diapositive suivante",
goToSlide: (index) => `Aller à la diapositive ${index + 1}`,
goToPage: (index) => `Aller à la page ${index + 1}`,
pagination: "Choisir une diapositive",
play: "Lancer le défilement",
stop: "Arrêter le défilement",
};
export function Nouveautes() {
return (
<Carousel aria-label="Nouveautés" labels={LABELS}>
…
</Carousel>
);
}En vanilla, passez le même objet à createCarousel et à bindCarouselControls.
API
Carousel
| Prop | Type | Défaut | Description |
|---|---|---|---|
orientation | CarouselOrientation | "horizontal" | Axe des slides : `"horizontal"` ou `"vertical"`. Un carrousel vertical a besoin d'une hauteur, sur `CarouselContent`. |
align | CarouselAlign | "start" | Où se pose une slide dans la zone visible : `"start"`, `"center"` ou `"end"`. `"center"` laisse voir les voisines des deux côtés. |
perView | number | 1 | Nombre de slides visibles à la fois, de 1 à 12, décimales acceptées (1,2 laisse dépasser la suivante). Valeur de base : la variable `--carousel-per-view` la remplace (`md:[--carousel-per-view:3]`), et `basis-auto` avec une largeur sur une slide l'emporte sur les deux. Sans effet avec `effect="fade"`. |
gap | string | number | 16 | Espace entre les slides : un nombre de pixels CSS, de 0 à 200, ou une longueur CSS (`"1.5rem"`). Valeur de base : la variable `--carousel-gap` la remplace. |
loop | boolean | false | Fait tourner le carrousel sans fin, sans copier les slides. Il faut assez de slides pour remplir la zone visible plus une ; sinon le carrousel reste borné, et la boucle revient dès que la place le permet. |
step | CarouselStep | "page" | Ce dont avancent les flèches, les points, le clavier et l'autoplay : `"page"`, toutes les slides qui tiennent entièrement, ou `"slide"`, une seule. |
swipe | CarouselSwipe | "page" | Jusqu'où un lancer emporte le contenu : `"page"`, une longueur visible au plus depuis la slide de départ ; `"free"`, aussi loin que l'élan ; `"single"`, une étape par geste. |
snap | boolean | true | Pose le contenu sur une étape. Avec `false`, il glisse librement et s'arrête où son élan s'épuise ; les flèches et les points visent toujours des étapes. |
containScroll | boolean | true | Hors boucle, garde chaque étape dans le contenu : aucun vide au début ni à la fin. Désactivez-le pour que la première et la dernière slide puissent se poser au centre. |
effect | CarouselEffect | "none" | Effet intégré : `"none"`, `"scale"` (les slides rétrécissent vers la slide sélectionnée en s'éloignant de leur place, jusqu'à 86 %) ou `"fade"` (fondu enchaîné de slides superposées, de même taille ; un saut lointain va droit à la slide visée). |
duration | number | 0.5 | Durée visuelle d'un déplacement, en secondes, de 0,15 à 2. Un lancer arrive plus vite quand son élan l'exige. |
bounce | number | 0 | Rebond à l'arrivée, de 0 (aucun) à 0,5. |
startIndex | number | 0 | Slide montrée au départ, à partir de 0, ramenée au nombre de slides. Seulement au premier rendu ; `index` l'emporte s'il est donné. |
index | number | undefined | Slide sélectionnée, contrôlée par l'application, à partir de 0 : quand elle change, le carrousel s'y rend. Mettez-la à jour depuis `onIndexChange`. |
autoplay | boolean | false | Fait avancer le carrousel seul, d'une étape toutes les `interval` secondes. En mouvement réduit, il attend que la personne le lance. Ajoutez `CarouselAutoplayToggle`. |
interval | number | 5 | Temps passé sur chaque étape avec `autoplay`, en secondes, de 1 à 60. |
paused | boolean | false | Met l'autoplay en pause, là où en est son délai. Pour toutes les animations de la page, voir `setAnimationsPaused` (FrameLoop). |
draggable | boolean | true | Permet de glisser le contenu à la souris, au stylet et au doigt. |
wheel | boolean | true | Permet de faire défiler le contenu au trackpad ou à la molette, le long de son axe. Un défilement vertical sur un carrousel horizontal reste à la page. |
selectOnClick | boolean | false | Un clic sur une slide l'amène à sa place (une voisine au centre, par exemple). Laissez-le désactivé si les slides contiennent des liens. |
labels | Partial<CarouselLabels> | undefined | Textes lus par les lecteurs d'écran, fusionnés avec les textes anglais par défaut. Voir [Traduire les textes](#traduire-les-textes). |
disabled | boolean | false | Fige le carrousel là où il est : ni geste, ni clavier, ni autoplay, et des contrôles inactifs. Le focus clavier sur une slide cachée l'amène quand même dans la vue, sans animation. |
reducedMotion | ReducedMotion | "user" | `"user"` suit le réglage du système (fondu court au lieu du glissement, autoplay en attente), `"always"` l'impose, `"never"` l'ignore (à réserver à un choix explicite du visiteur). |
onIndexChange | ((index: number) => void) | undefined | Appelé avec la nouvelle slide sélectionnée dès que la destination est choisie, quelle qu'en soit la cause : les points et votre état réagissent sans attendre la fin du mouvement. `update:index` en Vue. |
onSettle | ((index: number) => void) | undefined | Appelé avec la slide sélectionnée quand le carrousel s'arrête. `settle` en Vue. |
Un nombre hors de sa plage est ramené à la borne la plus proche. Une valeur invalide (NaN, mauvais type, valeur hors de la liste) est ignorée : l'option garde sa valeur courante. Pour gap, une chaîne qui ne contient qu'un nombre (gap="24" dans un template Vue) compte en pixels.
En React, className, style et les autres attributs de chaque partie passent à son élément, et la ref le désigne ; Carousel accepte aussi actionsRef. En Vue, class et les attributs passent de la même façon ; la ref de Carousel expose el et les actions.
Les parties
| Partie | Élément | Props propres |
|---|---|---|
CarouselContent | la zone visible | trackClassName (track-class en Vue) : les classes de la piste qui se déplace |
CarouselItem | une slide | Un aria-label remplace le nom « 3 of 8 » posé par le carrousel |
CarouselPrevious, CarouselNext | un bouton | Une icône en enfant (slot par défaut en Vue) |
CarouselDots | un groupe de boutons | Un point par étape |
CarouselProgress | une barre décorative | variant : "position" (par défaut, la part visible du contenu) ou "autoplay" (le délai) |
CarouselCounter | « 2 / 6 » | Compte les étapes (quatre cartes par page sur douze : « 1 / 3 ») ; lu comme la slide sélectionnée |
CarouselAutoplayToggle | un bouton | Rendu seulement avec autoplay ; une icône en enfant |
CarouselThumbnails | une bande de boutons | orientation : "horizontal" (par défaut) ou "vertical" (donnez-lui une hauteur) |
CarouselThumbnail | une miniature | Nommée par l'alt de son image, ou un aria-label ; désigne une slide, jamais une page |
L'état de useCarousel
| Prop | Type | Défaut | Description |
|---|---|---|---|
index | number | — | La slide sélectionnée, à partir de 0. |
count | number | — | Le nombre de slides. |
snap | number | — | L'étape sélectionnée (le point actif), à partir de 0. |
snapCount | number | — | Le nombre d'étapes (de points). 0 tant que le carrousel n'a pas mesuré ses slides. |
canPrev | boolean | — | Une étape précédente existe (toujours vrai en boucle). Faux quand `disabled`. |
canNext | boolean | — | Une étape suivante existe (toujours vrai en boucle). Faux quand `disabled`. |
playing | boolean | — | L'autoplay tourne : activé, avec plus d'une étape, ni arrêté par la personne ni mis en attente par le mouvement réduit. |
autoplay | boolean | — | L'option `autoplay` est activée. |
disabled | boolean | — | L'option `disabled` est activée : les contrôles sont inactifs. |
Accessibilité
Ce que Carousel fait seul :
- Le motif WAI-ARIA « Carousel ». La racine est une région (
role="region",aria-roledescription="carousel"), chaque slide un groupe nommé « 3 of 8 ». Les flèches et les points sont nommés et reliés à la piste (aria-controls), le point actif portearia-current. - Des annonces sobres. Quand la personne déplace le carrousel, une région d'annonce dit « slide 3 of 8 » une fois arrivé. Elle reste muette pendant l'autoplay.
- Le clavier. La zone visible est focalisable : les flèches avancent d'une étape (inversées en RTL),
DébutetFinvont aux extrémités. Le contenu des slides cachées et les miniatures cachées restent dans l'ordre de tabulation : le focus clavier sur l'un d'eux l'amène dans la vue, sans faire défiler la page, même quand le carrousel est désactivé. - Des contrôles qui gardent le focus. Au bout du carrousel, les flèches passent en
aria-disabledplutôt qu'endisabled: le focus clavier reste sur le bouton. Avecdisabled, les points, les miniatures et le bouton de rotation passent aussi enaria-disabled. - Des cibles de 24 pixels pour les points (WCAG 2.5.8), même si leur marque est plus petite.
- Le mouvement réduit remplace le glissement par un fondu de 0,25 seconde ; un glisser se pose vite, sans élan ni rebond, sur l'étape la plus proche (ou, lancé, sur la suivante dans son sens), et les effets restent dans leur état de repos. L'autoplay attend que la personne le lance. Les transitions des contrôles (le point qui s'étire, le compteur) suivent la même politique,
reducedMotioncompris. Un changement du réglage du système s'applique aussitôt. - Le contraste élevé (
forced-colors) : les points, la barre et la miniature active gardent des couleurs système visibles. - L'autoplay s'arrête au focus clavier et à toute navigation de la personne, se met en pause au survol, et respecte la pause commune de la page.
- Sans JavaScript, la zone visible défile nativement : tout le contenu reste accessible.
Ce que l'intégrateur doit faire :
- Nommer le carrousel (
aria-labelouaria-labelledby). - Offrir une alternative au glisser (WCAG 2.5.7) : des flèches, des points ou des miniatures, utilisables d'un seul clic.
- Ajouter
CarouselAutoplayToggleavecautoplay, avantCarouselContentdans l'ordre du document. - Un texte de remplacement sur chaque image qui informe, et un nom pour chaque miniature (l'
altde son image suffit). Une image décorative, dans un lien déjà nommé par son texte, prendalt="", comme dans la rangée de produits. - Garder le contraste du texte sur les slides voisines : un effet d'opacité ne descend pas sous 0,6 sur du texte.
- Traduire les textes avec
labelssi la page n'est pas en anglais.
Performances
- Aucune dépendance. Le cœur et ses fichiers annexes pèsent environ 13 kB minifiés et compressés (33 kB minifiés). Avec FrameLoop et PointerTracker, partagés avec les autres éléments d'O-UI, l'ensemble pèse 14 kB ; l'adaptateur React complet, toutes parties comprises, 18 kB.
- Rien au repos. Le carrousel ne travaille que pendant un mouvement : au repos, ni boucle d'animation ni écriture.
- En mouvement, seules la position de la piste et les variables des slides changent, et seulement quand leur valeur change.
--carousel-progresset--carousel-visible, plus coûteuses, ne sont écrites que si un élément les lit. - Plusieurs carrousels sur une page se mesurent ensemble, en une seule passe.
- L'autoplay ne coûte presque rien : le délai et ses indicateurs sont animés par le navigateur, sans JavaScript à chaque frame.
- Hors de l'écran ou dans un onglet masqué, rien ne tourne : un mouvement en cours se termine aussitôt et l'autoplay se met en pause.
- Le texte reste net au repos : la piste n'occupe un calque à part (
will-change) que pendant le mouvement.
Le coût tient surtout à vos slides. Pour des dizaines de slides lourdes, préférez des images compressées aux bonnes dimensions ; le carrousel ne charge en avance que les voisines.
Problèmes fréquents
- Les anneaux de focus des cartes sont coupés. La zone visible coupe ce qui dépasse : donnez-lui
-m-1 p-1(règle 4 de l'utilisation). - Le carrousel élargit sa colonne de grille. Donnez-lui
min-w-0. - Trois cartes avec
basis-1/3débordent. L'espace s'ajoute aux largeurs : utilisezperView={3}ou[--carousel-per-view:3], qui en tiennent compte. - Le carrousel vertical ne bouge pas. Il lui faut une hauteur, sur
CarouselContent. - La boucle ne tourne pas. Il manque des slides : il en faut assez pour remplir la zone visible plus une.
- Le fondu mélange des slides de tailles différentes. Avec
effect="fade", les slides se superposent : donnez-leur la même taille. - Un clic sur une slide déclenche aussi son lien avec
selectOnClick. RéservezselectOnClickaux slides sans lien, ou à des miniatures. - L'autoplay ne démarre pas. Le système demande moins de mouvement : l'autoplay attend que la personne le lance, avec
CarouselAutoplayToggle. - Next.js refuse une fonction passée au carrousel.
onIndexChange,onSettleou deslabelsavec des fonctions demandent un composant"use client"autour du carrousel.
Notes et limites
- Navigateurs : ceux de Tailwind CSS v4 (Chrome et Edge 111+, Safari 16.4+, Firefox 128+).
- Slide de départ : le rendu serveur montre la première slide. Avec
startIndexouindex, le carrousel saute à la slide demandée à l'hydratation, sans animation : ce saut peut se voir pendant le chargement. Si la première image vue compte, placez cette slide en premier. - Piste : les slides sont alignées en ligne (ou en colonne) depuis le début de la piste. Un
justify-contentsur la piste n'est pas pris en compte. - Sens de lecture :
dir="rtl"est géré (glisser, flèches du clavier, icônes en miroir). Les modes d'écriture verticaux ne le sont pas. - Slides masquées : une slide en
display: nonene compte pas, et la sélection se reporte sur les autres. - Carrousels imbriqués : un carrousel dans une slide garde ses propres gestes ; le carrousel extérieur ne bouge pas.
- Un seul pointeur guide le glisser. Le zoom à deux doigts reste au navigateur.