Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

Carousel

BêtaComposant

Un 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
Paramètres

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.json

Carousel 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 :

  1. Nommez le carrousel avec aria-label, ou aria-labelledby vers son titre : c'est une région de la page, et un lecteur d'écran l'annonce par ce nom.
  2. Réglez la taille des slides sur la racine. perView (1 par défaut, décimales acceptées) et gap (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-lui basis-auto avec sa largeur (basis-auto w-72) : basis-auto annule la part calculée par le carrousel, puis le carrousel mesure ce qui est rendu.
  3. 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 en relative).
  4. 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é.
  5. Dans une grille ou une rangée flex, donnez min-w-0 au carrousel. Sinon la piste, plus large que la zone visible, élargit sa colonne.
  6. En vertical, donnez une hauteur à CarouselContent (h-96). Chaque slide en prend sa part selon perView.
  7. Donnez aux images leurs dimensions (width, height ou 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'ajouter draggable="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 :

VariableOùValeur
--carousel-slide-progresschaque slideDe −1 à 1 : 0 à sa place, −1 une slide avant, 1 une slide après
--carousel-slide-distancechaque slideDe 0 à 1 : l'éloignement de sa place
--carousel-progressracineDe 0 à 1 : la position dans le contenu
--carousel-visibleracineDe 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)] sur CarouselItem. 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) et data-in-view (visible à plus de moitié) ;
  • sur la racine : data-dragging, data-can-prev, data-can-next, data-loop et data-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, des labels avec slideLabel…) ne peuvent pas venir d'un Server Component : placez alors le carrousel dans un composant à vous, marqué "use client". Un Link de Next.js fonctionne dans une slide, et un glisser ne le déclenche pas. next/image aussi : avec fill, donnez à CarouselItem une 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 :

  • index dé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.
  • index ne fige pas le carrousel : la personne le déplace toujours. onIndexChange signale chaque nouvelle slide, quelle qu'en soit la cause : un geste, un contrôle, l'autoplay, ou votre propre index (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 : onIndexChange n'est appelé que pour une slide nouvelle.
  • onSettle (settle en Vue) signale l'arrêt. startIndex ne choisit que la slide de départ ; index l'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

PropTypeDéfautDescription
orientationCarouselOrientation"horizontal"Axe des slides : `"horizontal"` ou `"vertical"`. Un carrousel vertical a besoin d'une hauteur, sur `CarouselContent`.
alignCarouselAlign"start"Où se pose une slide dans la zone visible : `"start"`, `"center"` ou `"end"`. `"center"` laisse voir les voisines des deux côtés.
perViewnumber1Nombre 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"`.
gapstring | number16Espace 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.
loopbooleanfalseFait 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.
stepCarouselStep"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.
swipeCarouselSwipe"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.
snapbooleantruePose 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.
containScrollbooleantrueHors 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.
effectCarouselEffect"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).
durationnumber0.5Durée visuelle d'un déplacement, en secondes, de 0,15 à 2. Un lancer arrive plus vite quand son élan l'exige.
bouncenumber0Rebond à l'arrivée, de 0 (aucun) à 0,5.
startIndexnumber0Slide 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é.
indexnumberundefinedSlide 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`.
autoplaybooleanfalseFait avancer le carrousel seul, d'une étape toutes les `interval` secondes. En mouvement réduit, il attend que la personne le lance. Ajoutez `CarouselAutoplayToggle`.
intervalnumber5Temps passé sur chaque étape avec `autoplay`, en secondes, de 1 à 60.
pausedbooleanfalseMet l'autoplay en pause, là où en est son délai. Pour toutes les animations de la page, voir `setAnimationsPaused` (FrameLoop).
draggablebooleantruePermet de glisser le contenu à la souris, au stylet et au doigt.
wheelbooleantruePermet 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.
selectOnClickbooleanfalseUn 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.
labelsPartial<CarouselLabels>undefinedTextes lus par les lecteurs d'écran, fusionnés avec les textes anglais par défaut. Voir [Traduire les textes](#traduire-les-textes).
disabledbooleanfalseFige 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.
reducedMotionReducedMotion"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)undefinedAppelé 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)undefinedAppelé 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émentProps propres
CarouselContentla zone visibletrackClassName (track-class en Vue) : les classes de la piste qui se déplace
CarouselItemune slideUn aria-label remplace le nom « 3 of 8 » posé par le carrousel
CarouselPrevious, CarouselNextun boutonUne icône en enfant (slot par défaut en Vue)
CarouselDotsun groupe de boutonsUn point par étape
CarouselProgressune barre décorativevariant : "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
CarouselAutoplayToggleun boutonRendu seulement avec autoplay ; une icône en enfant
CarouselThumbnailsune bande de boutonsorientation : "horizontal" (par défaut) ou "vertical" (donnez-lui une hauteur)
CarouselThumbnailune miniatureNommée par l'alt de son image, ou un aria-label ; désigne une slide, jamais une page

L'état de useCarousel

PropTypeDéfautDescription
indexnumber—La slide sélectionnée, à partir de 0.
countnumber—Le nombre de slides.
snapnumber—L'étape sélectionnée (le point actif), à partir de 0.
snapCountnumber—Le nombre d'étapes (de points). 0 tant que le carrousel n'a pas mesuré ses slides.
canPrevboolean—Une étape précédente existe (toujours vrai en boucle). Faux quand `disabled`.
canNextboolean—Une étape suivante existe (toujours vrai en boucle). Faux quand `disabled`.
playingboolean—L'autoplay tourne : activé, avec plus d'une étape, ni arrêté par la personne ni mis en attente par le mouvement réduit.
autoplayboolean—L'option `autoplay` est activée.
disabledboolean—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 porte aria-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ébut et Fin vont 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-disabled plutôt qu'en disabled : le focus clavier reste sur le bouton. Avec disabled, les points, les miniatures et le bouton de rotation passent aussi en aria-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, reducedMotion compris. 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-label ou aria-labelledby).
  • Offrir une alternative au glisser (WCAG 2.5.7) : des flèches, des points ou des miniatures, utilisables d'un seul clic.
  • Ajouter CarouselAutoplayToggle avec autoplay, avant CarouselContent dans l'ordre du document.
  • Un texte de remplacement sur chaque image qui informe, et un nom pour chaque miniature (l'alt de son image suffit). Une image décorative, dans un lien déjà nommé par son texte, prend alt="", 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 labels si 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-progress et --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/3 débordent. L'espace s'ajoute aux largeurs : utilisez perView={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éservez selectOnClick aux 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, onSettle ou des labels avec 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 startIndex ou index, 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-content sur 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: none ne 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.