Aller au contenu
Me contacter

Toutes les briques

Bac à sable

Le carrousel

En audit, je tombe sur des carrousels qui défilent tout seuls et emportent la carte que tu lisais encore. Ici, la liste avec scroll-snap ne bouge que si tu cliques sur une flèche ou que tu la fais défiler.

Technique
scroll-snap et boutons
Navigateurs
Chrome 105+, Firefox 121+, Safari 16+
JavaScript
19 lignes, pour les flèches

Démo

Les flèches font défiler la liste d'une vue. Le nombre de cartes visibles grandit avec la largeur du carrousel, et la carte suivante dépasse dans la marge pour montrer qu'il y a une suite.

Destinations

  • Lisbonne

    Sept collines, des tramways jaunes et des façades en azulejos.

  • Kyoto

    Plus de 1 600 temples bouddhistes et des jardins de mousse.

  • Montréal

    Une ville bilingue, construite autour du mont Royal.

  • Édimbourg

    Un château sur un ancien volcan, au-dessus de la vieille ville.

  • Marrakech

    Les souks de la médina et la place Jemaa el-Fna.

  • Reykjavik

    La capitale d'État la plus au nord du monde.

  • Oaxaca

    Des marchés colorés et une cuisine connue pour ses sept moles.

Anatomie

  1. section La région, nommée par son titre.
  2. h2 Le titre du carrousel.
  3. button Les 2 flèches, « Cartes précédentes » et « Cartes suivantes », masquées par hidden tant que le script n'a pas tourné.
  4. ul La zone qui défile, avec scroll-snap-type.
  5. li Une carte, qui se cale sur le bord gauche de la zone.
  6. a Le lien de la carte, celui que Tab atteint.

Code HTML

HTML
<section class="carousel" aria-labelledby="carousel-title">
  <h2 id="carousel-title">Destinations</h2>
  <!-- Flèches : masquées tant que le script n'a pas tourné -->
  <div class="carousel__arrows" hidden>
    <button
      type="button"
      data-direction="-1"
      aria-controls="carousel-track"
    >
      <span aria-hidden="true">←</span>
      <span class="visually-hidden">Cartes précédentes</span>
    </button>
    <button
      type="button"
      data-direction="1"
      aria-controls="carousel-track"
    >
      <span aria-hidden="true">→</span>
      <span class="visually-hidden">Cartes suivantes</span>
    </button>
  </div>
  <!-- Liste qui défile : une carte = un élément de liste -->
  <ul class="carousel__track" id="carousel-track">
    <li>
      <h3><a href="https://fr.wikipedia.org/wiki/Lisbonne">Lisbonne</a></h3>
      <p>…</p>
    </li>
    <li>…</li>
  </ul>
</section>

<script>
  // Flèches : défilent d'une vue, sans déplacer le focus
  const track = document.querySelector('.carousel__track');
  const arrows = document.querySelector('.carousel__arrows');
  const [prev, next] = arrows.querySelectorAll('button');
  const update = () => {
    const atStart = track.scrollLeft < 1;
    const atEnd = track.scrollLeft + track.clientWidth > track.scrollWidth - 1;
    prev.setAttribute('aria-disabled', atStart);
    next.setAttribute('aria-disabled', atEnd);
  };
  arrows.addEventListener('click', (e) => {
    const button = e.target.closest('button');
    if (button && button.getAttribute('aria-disabled') !== 'true') {
      track.scrollBy({ left: button.dataset.direction * track.clientWidth });
    }
  });
  track.addEventListener('scroll', update);
  window.addEventListener('resize', update);
  arrows.hidden = false;
  update();
</script>
CSS
/* Le carrousel mesure sa propre largeur (container query),
   pas celle de l'écran */
.carousel {
  container-type: inline-size;
}

.carousel__track {
  --visible: 1;
  --peek: 32px;
  --gap: 12px;
  display: flex;
  gap: var(--gap);
  /* La carte suivante dépasse dans la marge */
  padding-inline: var(--peek);
  scroll-padding-inline: var(--peek);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.carousel__track li {
  flex: 0 0 calc((100% - (var(--visible) - 1) * var(--gap)) / var(--visible));
  scroll-snap-align: start;
}

@container (min-width: 480px) {
  .carousel__track {
    --visible: 2;
    --peek: 36px;
  }
}

@container (min-width: 720px) {
  .carousel__track {
    --visible: 3;
  }
}

@container (min-width: 960px) {
  .carousel__track {
    --visible: 4;
    --peek: 48px;
  }
}

/* Avec les flèches, la barre de défilement n'est plus utile */
.carousel:has(.carousel__arrows:not([hidden])) .carousel__track {
  scrollbar-width: none;
}

.carousel__arrows button[aria-disabled="true"] {
  opacity: 0.55;
  cursor: default;
}

@media (prefers-reduced-motion: no-preference) {
  .carousel__track {
    scroll-behavior: smooth;
  }
}

Usages

Recommandé pour

  • Une série de contenus de même nature, que l'on parcourt dans l'ordre.
  • Des cartes qui mènent chacune à une page.

Déconseillé pour

  • Une lecture automatique. Sans moyen de l'arrêter et de la relancer, elle ne respecte pas le critère 13.8.
  • Masquer la barre de défilement sans flèches. À la souris sans trackpad, plus rien de visible ne fait défiler la liste.
  • Désactiver une flèche avec disabled. En bout de liste, le bouton perd le focus, qui retombe sur le body de la page.
  • Une information importante qu'on ne voit qu'en faisant défiler.

Accessibilité

Rôles et attributs ARIA

3 attributs ARIA : aria-labelledby nomme la région, aria-disabled signale une flèche en bout de liste, aria-hidden tait le caractère de la flèche. Pas d'aria-roledescription="carrousel" : à l'entrée, le lecteur d'écran annonce déjà « liste, 7 éléments », ce qui dit comment la parcourir.

AttributÉlémentDescription
aria-labelledby="carousel-title"sectionLa région prend le nom de son titre, « Destinations », et apparaît dans la liste des repères.
hiddendiv des flèchesRetiré par le script au chargement. Sans JavaScript, pas de flèches qui ne marcheraient pas, et la barre de défilement reste visible.
class="visually-hidden"span dans chaque buttonNomme les flèches « Cartes précédentes » et « Cartes suivantes » sans afficher le texte.
aria-hidden="true"span de la flècheLe caractère « ← » ou « → » n'est pas lu : le nom du bouton suffit.
aria-controls="carousel-track"buttonRelie chaque flèche à la liste qu'elle fait défiler, par l'id de la liste.
aria-disabled="true"buttonEn bout de liste, la flèche reste focalisable et le lecteur d'écran l'annonce « estompé ».
Rôle list (implicite)ulLe lecteur d'écran annonce le nombre de cartes dès l'entrée : « liste, 7 éléments ».

Navigation au clavier

ToucheAction
TabPasse des flèches au lien de la 1re carte, puis de carte en carte, et la liste défile pour afficher le lien
Maj+TabRevient au lien de la carte précédente
Entrée ou Espace sur une flècheFait défiler la liste d'une vue, sans déplacer le focus
Flèche gauche ou Flèche droite sur un lien de carteFait défiler la liste d'une carte, sans déplacer le focus. Le lien peut alors sortir du champ

Ordre de tabulation

  1. La flèche « Cartes précédentes », estompée au départ.
  2. La flèche « Cartes suivantes ».
  3. Le lien « Lisbonne ».
  4. Le lien « Kyoto ».
  5. Le lien « Montréal ».
  6. Le lien « Édimbourg », puis ceux des 3 cartes suivantes, hors champ au départ : la liste défile quand un lien reçoit le focus.

Avec un lecteur d'écran

Exemple sur Chrome avec VoiceOver. Les mots changent d'un lecteur d'écran à l'autre.

ActionAnnonce
Entrée dans la région« Destinations, région »
Tab sur la 1re flèche« Cartes précédentes, estompé, bouton »
Tab, puis Entrée« Cartes suivantes, bouton » ; le défilement n'est pas annoncé, la liste reste lisible élément par élément
Entrée dans la liste« liste, 7 éléments »

Critères RGAA concernés

Critère 7.1 : Chaque script est-il, si nécessaire, compatible avec les technologies d’assistance ?
Les flèches sont de vrais boutons : le lecteur d'écran reçoit leur nom, leur rôle et l'état aria-disabled.
Critère 7.3 : Chaque script est-il contrôlable par le clavier et par tout dispositif de pointage (hors cas particuliers) ?
Les flèches s'activent au clavier, avec Entrée ou Espace, comme à la souris.
Critère 9.3 : Dans chaque page web, chaque liste est-elle correctement structurée ?
Les cartes forment une vraie liste, ul et li.
Critère 10.7 : Dans chaque page web, pour chaque élément recevant le focus, la prise de focus est-elle visible ?
Chaque lien qui reçoit le focus avec Tab est ramené dans la zone visible, où son focus se voit.
Critère 12.8 : Dans chaque page web, l’ordre de tabulation est-il cohérent ?
L'ordre de tabulation suit l'ordre visuel : les flèches, puis les cartes.
Critère 13.8 : Dans chaque page web, chaque contenu en mouvement ou clignotant est-il contrôlable par l’utilisateur ?
Rien ne bouge sans action de l'utilisateur.

Autres références

Au-delà de l'APG

NouveautéApportNavigateursDans la démo
::scroll-button()Génère en CSS les flèches précédente et suivante, nommées par le texte alternatif de content.Chrome 135+, Edge 135+Variation expérimentale
::scroll-markerGénère en CSS un point de navigation par carte, relié à sa carte.Chrome 135+, Edge 135+Non

Variations

Flèches générées en CSS, Expérimental

::scroll-button() crée les flèches sans JavaScript, nommées par le texte alternatif de content. Seuls Chrome et Edge les génèrent, depuis avril 2025 : ailleurs, garde la barre de défilement. En bout de liste, le navigateur les désactive et le focus retombe sur la page, comme avec disabled : en production, garde plutôt les flèches de la démo.

Destinations, flèches en CSS

  • Lisbonne

    Sept collines, des tramways jaunes et des façades en azulejos.

  • Kyoto

    Plus de 1 600 temples bouddhistes et des jardins de mousse.

  • Montréal

    Une ville bilingue, construite autour du mont Royal.

  • Édimbourg

    Un château sur un ancien volcan, au-dessus de la vieille ville.

  • Marrakech

    Les souks de la médina et la place Jemaa el-Fna.

  • Reykjavik

    La capitale d'État la plus au nord du monde.

  • Oaxaca

    Des marchés colorés et une cuisine connue pour ses sept moles.

Les flèches n'apparaissent que dans Chrome et Edge. Ailleurs, la barre de défilement reste. Va au bout de la liste avec Entrée : le focus quitte la flèche.

Code
/* En bout de liste, le navigateur désactive la flèche :
   le focus retombe sur la page */
.carousel__track::scroll-button(left) {
  content: "←" / "Cartes précédentes";
}

.carousel__track::scroll-button(right) {
  content: "→" / "Cartes suivantes";
}

Une carte par vue

flex: 0 0 100% n'affiche qu'une carte, et padding-inline: 0 retire la marge où dépassait la suivante. Les flèches deviennent alors le seul indice visible qu'il y a une suite.

Destinations, une carte par vue

  • Lisbonne

    Sept collines, des tramways jaunes et des façades en azulejos.

  • Kyoto

    Plus de 1 600 temples bouddhistes et des jardins de mousse.

  • Montréal

    Une ville bilingue, construite autour du mont Royal.

  • Édimbourg

    Un château sur un ancien volcan, au-dessus de la vieille ville.

  • Marrakech

    Les souks de la médina et la place Jemaa el-Fna.

  • Reykjavik

    La capitale d'État la plus au nord du monde.

  • Oaxaca

    Des marchés colorés et une cuisine connue pour ses sept moles.

Code
/* Sans marge, la carte suivante ne dépasse plus */
.carousel__track {
  padding-inline: 0;
  scroll-padding-inline: 0;
}

.carousel__track li {
  flex: 0 0 100%;
}

Grille sur grand écran

Avec une container query, la même liste passe en grille quand le carrousel est assez large. Toutes les cartes s'affichent, et les flèches disparaissent puisque plus rien ne défile.

Destinations, en grille

  • Lisbonne

    Sept collines, des tramways jaunes et des façades en azulejos.

  • Kyoto

    Plus de 1 600 temples bouddhistes et des jardins de mousse.

  • Montréal

    Une ville bilingue, construite autour du mont Royal.

  • Édimbourg

    Un château sur un ancien volcan, au-dessus de la vieille ville.

  • Marrakech

    Les souks de la médina et la place Jemaa el-Fna.

  • Reykjavik

    La capitale d'État la plus au nord du monde.

  • Oaxaca

    Des marchés colorés et une cuisine connue pour ses sept moles.

Code
@container (min-width: 520px) {
  .carousel__track {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    overflow: visible;
  }

  /* Plus rien ne défile : les flèches disparaissent */
  .carousel__arrows {
    display: none;
  }
}