
Publié par Fabrice le 5 mai 2026 - Temps de lecture estimé : 23 minutes
Catégories :
Webmastering
Étiquettes :
accessibilité
CSS
JavaScript
RGAA
snippet
Vanilla JS
Le temps était venu de moderniser ce composant JavaScript, même si son utilité reste toujours discutable !
On pourrait longuement palabrer sur l'utilité d'un carrousel, mais en attendant l'arrivée du masonry layout avec display: grid-lanes; et de sa mise en page façon Pinterest - voir d'opter pour un défilement horizontal, l'élaboration de ce type de projet demeure une expérience enrichissante du fait des compétences mises en œuvre : accessibilité, CSS et JavaScript.
Cette nouvelle version est évidemment responsive et entièrement en langages natifs. De plus, ce composant fonctionne avec des images de différentes tailles, elles sont contenues à l'intérieur du bloc et centrées verticalement et horizontalement : la magie de object-fit couplé à grid.
Rappel : l'accessibilité d'un carrousel
Vous pouvez vous référer à l'article de la première version du carrousel pour des explications plus exhaustives.
- Les contenus en mouvement doivent être contrôlables : bouton pause en début de code et raccourcis clavier pour le contrôle du carrousel
- Zone live ARIA :
aria-live="polite"et focus sur l'élément actif permet la lecture du panneau en cours - Attributs ARIA :
aria-controls,aria-selectedetrole="group"sur les boutons de la navigation par points,aria-roledescriptionpour indiquer le rôle de chaque contenu,aria-labelcomplète les informations - Arrêt du carrousel à la prise de focus
La base HTML et ses nouveautés
- Séparation du carrousel et des éléments de contrôles, le bouton d'arrêt se situant en premier dans l'arbre DOM
- Ajout de
aria-live="off"par défaut sur le conteneur - Ajout de
tabindexsur chaque panneau (0 pour le panneau actif et -1 pour les autres)
<section id="slides" aria-roledescription="Slideshow" aria-label="Description of Carousel">
<!-- Slide Controls -->
<div class="slides-panel-control">
<div class="slides-control">
<button type="button" class="slides-playpause" aria-controls="slides-items" aria-label="Stop Carousel" disabled>
<svg class="playpause" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img">
<g class="play">
<path d="m7.43 31c2.36 0 4.29-1.93 4.29-4.29v-21.4c0-2.36-1.93-4.29-4.29-4.29-2.36 0-4.29 1.93-4.29 4.29v21.4c0 2.36 1.93 4.29 4.29 4.29zm12.9-25.7v21.4c0 2.36 1.93 4.29 4.29 4.29s4.29-1.93 4.29-4.29v-21.4c0-2.36-1.93-4.29-4.29-4.29s-4.29 1.93-4.29 4.29z"></path>
</g>
<g class="pause">
<path d="m6.29 0.988c1.75-0.00742 13.3 8.37 20 12.4 2.61 1.35 2.72 3.64 0.145 5.18-6.19 3.67-18.7 12.4-20.3 12.4-1.59 0.0055-2.31-1.16-2.36-2.73 0.0104-8.27-0.0208-16.5 0.0156-24.8-0.0349-1.19 0.787-2.46 2.53-2.47z"></path>
</g>
</svg>
</button>
<button type="button" class="slides-prev" aria-controls="slides-items" aria-label="Previous Slide" disabled>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img">
<path d="m22.1 31c2.03-0.108 3.01-2.54 1.62-4.03l-10.4-10.8 10.4-11c2.34-2.26-1.13-5.73-3.39-3.39l-12 12.6c-0.903 0.904-0.937 2.36-0.0775 3.3l12 12.5c0.483 0.548 1.19 0.846 1.92 0.808z"></path>
</svg>
</button>
<button type="button" class="slides-next" aria-controls="slides-items" aria-label="Next Slide" disabled>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img">
<path d="m9.9 31c-2.03-0.108-3.01-2.54-1.62-4.03l10.4-10.8-10.4-11c-2.34-2.26 1.13-5.73 3.39-3.39l12 12.6c0.903 0.904 0.937 2.36 0.0775 3.3l-12 12.5c-0.483 0.548-1.19 0.846-1.92 0.808z"></path>
</svg>
</button>
</div>
</div>
<div id="slides-items" aria-live="off">
<!-- Slide 1 // Active slide: class="slideactive" and tabindex="0" -->
<div id="slide-1" class="slide-item slideactive" role="group" aria-roledescription="Slide" aria-label="1 of 3" tabindex="0">
<figure>
<img src="img1.webp" alt="Alternative image 1" width="1200" height="600" loading="lazy" decoding="async">
</figure>
<figcaption>Slide 1 Description</figcaption>
</div>
<!-- Slide 2 -->
<div id="slide-2" class="slide-item" role="group" aria-roledescription="Slide" aria-label="2 of 3" tabindex="-1">
<figure>
<img src="img2.webp" alt="Alternative image 2" width="556" height="247" loading="lazy" decoding="async">
</figure>
<figcaption>Slide 2 Description</figcaption>
</div>
<!-- Slide 3 -->
<div id="slide-3" class="slide-item" role="group" aria-roledescription="Slide" aria-label="3 of 3" tabindex="-1">
<figure>
<img src="img3.webp" alt="Alternative image 3" width="1200" height="1800" loading="lazy" decoding="async">
</figure>
<figcaption><strong>Slide 3 Description.</strong><br><span lang="lat">Venerem elegerit sexus mercenariae tabernaculum matrimonii in conductae post species.</span></figcaption>
</div>
</div>
</section>
Le code CSS et ses nouveautés
- Passage en Vanilla CSS et ajout de
@layerpour une meilleure intégration dans les projets - Effet de transition avec
transition-behavior: allow-discrete;et@starting-stylepour un support complet - Exit le positionnement relatif et absolu, place à
gridstack etflex - Utilisation de
display: none;sur les panneaux inactifs pour éviter l'interprétation par un lecteur d'écran - Ajout de
object-fitpour contraindre l'image à l'intérieur du conteneur
@layer resetdemo, carousel;
@layer resetdemo {
*,
*::before,
*::after {
box-sizing: border-box;
min-width: 0;
}
body {
margin: 0;
font-size: 1rem;
font-family: system-ui, sans-serif;
line-height: 1.5;
}
img, figure {
max-width: 100%;
height: auto;
margin: 0;
}
svg {
vertical-align: middle;
}
button {
padding: 0;
margin: 0;
border-style: none;
touch-action: manipulation;
display: inline-block;
border: none;
background: none;
cursor: pointer;
&:focus-visible {
outline: max(2px, .08em) dashed currentColor;
outline-offset: max(2px, .08em);
}
}
}
@layer carousel {
:root {
--carousel-width: 1200px;
--carousel-aspect-ratio: 12/6;
--carousel-color: DarkOrchid;
--carousel-bg: AntiqueWhite;
}
#slides {
max-width: var(--carousel-width);
margin: 3.618em auto;
.slides-panel-control {
display: flex;
justify-content: space-between;
}
/* Control Buttons */
.slides-control, .slides-dots {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 1em;
gap: 1em;
}
.slides-control {
button {
background-color: rgba(0, 0, 0, 0.7);
border-radius: .3em;
display: inline-block;
padding: 1em;
svg {
width: 1.618em;
height: 1.618em;
}
&:disabled {
display: none;
}
&:hover {
background-color: var(--carousel-color);
}
:is(svg, g) {
pointer-events: none;
fill: #fff;
}
}
.slides-playpause .play,
.slides-playpause .playpause.paused .pause {
display: none;
}
.slides-playpause .playpause.paused .play {
display: block;
}
}
/* Dots Buttons */
.slides-dots {
button {
display: inline-block;
padding: 1em;
background-color: transparent;
}
svg {
pointer-events: none;
width: 1.618em;
height: 1.618em;
fill: rgba(0, 0, 0, 0.7);
}
:is(button:hover, button:focus, button:active) svg {
fill: transparent;
stroke: var(--carousel-color);
stroke-width: .3ch;
}
button[aria-selected="true"] svg {
fill: var(--carousel-color);
}
}
@media (width < 36rem) {
.slides-panel-control {
display: block;
}
}
}
#slides-items {
display: grid;
grid: 1fr / 1fr; /* Not necessary - for greater clarity */
> .slide-item {
grid-area: 1 / 1;
overflow: hidden; /* For responsive */
aspect-ratio: var(--carousel-aspect-ratio); /* For responsive */
display: grid;
grid: 1fr / 1fr; /* Not necessary - for greater clarity */
background: var(--carousel-bg);
> * {
grid-area: 1 / 1;
}
figure {
align-self: center;
justify-self: center;
max-height: calc(var(--carousel-width) / (var(--carousel-aspect-ratio)));
img {
aspect-ratio: var(--carousel-aspect-ratio); /* For responsive */
max-height: calc(var(--carousel-width) / (var(--carousel-aspect-ratio)));
object-fit: contain;
}
}
figcaption {
align-self: end;
font-size: clamp(.8em, .6em + .7vw, 1.2em);
text-align: center;
color: #fff;
padding: .9em;
background-color: rgba(0, 0, 0, 0.7);
}
}
.slideactive:focus-visible {
outline: max(2px, .08em) solid currentColor;
outline-offset: 0;
}
/* CSS Transition */
.slideactive {
opacity: 1;
transition: all 2s;
transition-behavior: allow-discrete;
@starting-style {
opacity: 0;
}
}
.slide-item:not(.slideactive) {
opacity: 0;
display: none;
}
}
}
Le code JavaScript et ses nouveautés
- Ajout du focus sur l'élément en cours et gestion de
tabindex(0 et -1) pour une meilleure restitution avec un lecteur d'écran - Détection de
prefers-reduced-motionet désactivation de la rotation automatique, si nécessaire - Ajout du swipe pour mobile
- Arrêt du carrousel à la prise de focus sur le panneau en cours lorsque la rotation automatique est activée
/*
* @Adilade Slideshow/Carousel
* @See www.adilade.fr/blog/carrousel-accessible-v2/
* @Version : 2.0
*
* Keyboard :
* Previous : left arrow or ctrl + left arrow
* Next : right arrow or ctrl + right arrow
* Pause/Play : space
*
* Free to use - No warranty
*/
var timeInterval = '7000'; // Time between slides in ms
var slidediv = document.querySelector('#slides');
var carousel = document.querySelector('#slides-items');
var items = document.querySelectorAll('.slide-item');
var panel = document.querySelector('.slides-control');
if (carousel !== undefined && items !== undefined && carousel !== null && items !== null) {
var itemscount = items.length;
var btnprev = document.querySelector('.slides-prev');
var btnnext = document.querySelector('.slides-next');
var btnplaypause = document.querySelector('.slides-playpause');
var btnplaypausepath = document.querySelector('.playpause');
// If JavaScript enabled
btnprev.removeAttribute('disabled');
btnnext.removeAttribute('disabled');
btnplaypause.removeAttribute('disabled');
if (itemscount > 1) {
// Create Dots
var dotbox = document.createElement('div');
dotbox.classList.add('slides-dots');
dotbox.setAttribute('role', 'group');
panel.after(dotbox);
for (var i=0; i<itemscount; i++) {
dotbox.insertAdjacentHTML('beforeend', '<button type="button" aria-controls="slide-'+(i+1)+'" aria-label="Slide number '+(i+1)+'" aria-selected="'+(document.querySelector('.slideactive').getAttribute('id').slice(-1) == (i+1) ? 'true' : 'false')+'"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img"><circle cx="16" cy="16" r="13" /></svg></button>');
}
var dots = document.querySelectorAll('.slides-dots button');
var playpause = null;
function slideprev() {
var itemcurrent = document.querySelector('.slideactive');
var dotcurrent = document.querySelector('.slides-dots button[aria-selected="true"]');
var prevslide = itemcurrent.previousElementSibling;
var prevdot = dotcurrent.previousElementSibling;
if(prevslide === null) {
prevslide = items[itemscount - 1];
prevdot = dots[itemscount - 1];
}
// Remove current
itemcurrent.classList.remove('slideactive');
itemcurrent.blur();
itemcurrent.setAttribute('tabindex', '-1');
dotcurrent.setAttribute('aria-selected', 'false');
// Add Next
prevslide.setAttribute('tabindex', '0');
prevslide.classList.add('slideactive');
prevslide.focus();
prevdot.setAttribute('aria-selected', 'true');
}
function slidenext() {
var itemcurrent = document.querySelector('.slideactive');
var dotcurrent = document.querySelector('.slides-dots button[aria-selected="true"]');
var nextslide = itemcurrent.nextElementSibling;
var nextdot = dotcurrent.nextElementSibling;
if(nextslide === null) {
nextslide = items[0];
nextdot = dots[0];
}
// Remove current
itemcurrent.setAttribute('tabindex', '-1');
itemcurrent.classList.remove('slideactive');
itemcurrent.blur();
dotcurrent.setAttribute('aria-selected', 'false');
// Add Next
nextslide.setAttribute('tabindex', '0');
nextslide.classList.add('slideactive');
nextslide.focus();
nextdot.setAttribute('aria-selected', 'true');
}
function slidepause() {
clearInterval(playpause);
playpause = null;
btnplaypause.setAttribute('aria-label', 'Play Carousel');
btnplaypausepath.classList.add('paused');
carousel.setAttribute('aria-live', 'off');
}
function slideplay() {
playpause = setInterval(slidenext, timeInterval);
btnplaypause.setAttribute('aria-label', 'Stop Carousel');
btnplaypausepath.classList.remove('paused');
}
function slideplaypause() {
if (playpause !== null) {
slidepause();
carousel.classList.add('btnpressed');
} else {
slideplay();
carousel.classList.remove('btnpressed');
//carousel.setAttribute('aria-live', 'off');
}
}
// Dots Navigate
[].map.call(dots, function(dot) {
dot.addEventListener('click', function(e) {
var itemcurrent = document.querySelector('.slideactive');
var dotcurrent = document.querySelector('.slides-dots button[aria-selected="true"]');
var dotclick = dot.getAttribute('aria-controls');
var targetslide = document.querySelector('#'+ dotclick +'');
var targetdot = document.querySelector('button[aria-controls="'+dotclick+'"]');
// Remove current
itemcurrent.setAttribute('tabindex', '-1');
itemcurrent.classList.remove('slideactive');
dotcurrent.setAttribute('aria-selected', 'false');
itemcurrent.blur();
// Add Target
targetslide.setAttribute('tabindex', '0');
targetslide.classList.add('slideactive');
targetdot.setAttribute('aria-selected', 'true');
targetslide.focus();
if (playpause !== null) {
slidepause();
}
carousel.setAttribute('aria-live', 'polite');
e.preventDefault();
});
});
// Navigate
btnprev.addEventListener('click', function() {
slideprev();
if (playpause !== null) {
slidepause();
}
carousel.setAttribute('aria-live', 'polite');
});
btnnext.addEventListener('click', function() {
slidenext();
if (playpause !== null) {
slidepause();
}
carousel.setAttribute('aria-live', 'polite');
});
btnplaypause.addEventListener('click', slideplaypause);
// Keyboard Navigate
slidediv.addEventListener('keydown', keyHandler);
function keyHandler(e) {
// Left Arrow
if (e.keyCode === 37 || (e.ctrlKey && e.keyCode === 37)) {
slideprev();
if (playpause !== null) {
slidepause();
}
e.preventDefault();
}
// Right Arrow
if (e.keyCode === 39 || (e.ctrlKey && e.keyCode === 39)) {
slidenext();
if (playpause !== null) {
slidepause();
}
e.preventDefault();
}
// Space
if (e.keyCode === 32) {
slideplaypause();
e.preventDefault();
}
}
// Mobile/Swipe Navigate
// Credit : https://www.kirupa.com/html5/detecting_touch_swipe_gestures.htm
carousel.addEventListener('touchstart', startTouch, false);
carousel.addEventListener('touchmove', moveTouch, false);
// Swipe Up / Down / Left / Right
var initialX = null;
var initialY = null;
function startTouch(e) {
initialX = e.touches[0].clientX;
initialY = e.touches[0].clientY;
};
function moveTouch(e) {
if (initialX === null) {
return;
}
if (initialY === null) {
return;
}
var currentX = e.touches[0].clientX;
var currentY = e.touches[0].clientY;
var diffX = initialX - currentX;
var diffY = initialY - currentY;
if (Math.abs(diffX) > Math.abs(diffY)) {
// sliding horizontally
if (diffX > 0) {
// swiped left
slidenext();
if (playpause !== null) {
slidepause();
}
} else {
// swiped right
slideprev();
if (playpause !== null) {
slidepause();
}
}
}
initialX = null;
initialY = null;
e.preventDefault();
};
// Animate Slides - Comment next line for disable auto-rotating
playpause = setInterval(slidenext, timeInterval);
// Stop Carousel on focusing slide only when Carousel auto-rotating
document.querySelector('.slideactive').addEventListener('focus', function() {
if(!carousel.classList.contains('btnpressed')) {
slidepause();
}
});
// Prefers-reduced-motion
var hasReducedMotion = (window.matchMedia('(prefers-reduced-motion: reduce)') === true || window.matchMedia('(prefers-reduced-motion: reduce)').matches === true);
if (hasReducedMotion) {
slidepause();
}
} else { // End itemscount < 1 => Remove buttons controls
btnprev.parentNode.removeChild(btnprev);
btnnext.parentNode.removeChild(btnnext);
btnplaypause.parentNode.removeChild(btnplaypause);
}
} // End if test