Een toegankelijke carrousel

Wil je een toegankelijke carrousel maken? In dit artikel leg ik uit hoe je dat doet.

Een toegankelijke carrousel zorgt ervoor dat iedereen je content kan bekijken en bedienen. Je hebt hiervoor drie dingen nodig: een goede HTML-structuur, de juiste aria-attributen en een logische bediening voor toetsenbordgebruikers.

Wat is een carrousel?

Een carrousel is een content-slider met meerdere afbeeldingen, teksten of andere inhoud die automatisch of handmatig kan worden doorgeklikt.

Structuur opbouwen

De basis van de carrousel bestaat uit verschillende onderdelen:

<section class="carousel">
  <!-- Bedieningselementen -->
  <!-- Container voor slides -->
  <!-- Indicator knoppen -->
</section>

Gebruik het <section>-element met beschrijvend aria-label als container. Dit maakt van de carrousel ook een eigen landmark en maakt duidelijk wat deze sectie bevat:

<section 
  class="carousel" 
  aria-label="Afbeeldingen van huisdieren"
  aria-roledescription="carrousel">

Het aria-roledescription-attribuut met de waarde carrousel geeft expliciet aan dat dit een carrousel is. Dit helpt mensen die gebruik maken van een schermlezer om te begrijpen wat voor component ze tegenkomen.

Navigatie

De bedieningselementen groepeer je in een container met role="group" met beschrijvend aria-label:

<div
  class="carousel-controls"
  role="group"
  aria-label="Bediening van de carrousel">

Elke knop krijgt een beschrijvend aria-label. De iconen worden verborgen voor schermlezers met aria-hidden="true":

<button
  type="button" 
  class="previous" 
  aria-label="Vorige dia">
  <span aria-hidden="true">⬅️</span>
</button>

Statusberichten

De dia’s plaats je in een <ul> omdat het een reeks gerelateerde items betreft. Een belangrijk onderdeel voor schermlezers is de live region:

<ul
  class="carousel-slides" 
  aria-live="polite">

Met aria-live="polite" kondig je veranderingen in de content aan voor schermlezers. De schermlezer vertelt dan bijvoorbeeld: “2 van 3: Een slapende kat op de vensterbank”.

Let op: Gebruik deze alleen als de carrousel niet automatisch beweegt, anders kies je beter voor aria-live="off".

Dia’s

Elke dia krijgt:

  • role="group": Vertelt hulptechnologieën dat deze elementen bij elkaar horen.
  • aria-roledescription="dia": Geeft context voor hulptechnologieën.
  • aria-labelledby: Koppelt de titel aan het dialoogvenster.
  • aria-current="true": Vertelt hulptechnologieën dat dit de actieve dia is.
  • aria-hidden="true": Verbergt inactieve dia’s voor hulptechnologieën.
<li 
  class="carousel-slide" 
  role="group"
  aria-roledescription="dia"
  aria-labelledby="dia1">
  <div id="dia1">
    <span class="sr-only">Dia 1 van 3</span>
    <img src="hond.jpg" alt="Een vrolijke hond in het park">
  </div>
</li>

Interactie

Gebruik JavaScript om de carrousel goed te laten werken:

Logica

Implementeer de carrousel-logica in je Javascript:

function goToSlide(index) {
  // Vorige slide verbergen
  slides[currentIndex].setAttribute('aria-current', 'false');
  slides[currentIndex].setAttribute('aria-hidden', 'true');
    
  // Nieuwe index instellen
  currentIndex = index;
    
  // Nieuwe slide tonen
  slides[currentIndex].setAttribute('aria-current', 'true');
  slides[currentIndex].setAttribute('aria-hidden', 'false');
}

Toetsenbord

Gebruikers de carrousel laten bewegen met de pijltoetsen:

carousel.addEventListener('keydown', function(e) {
  if (e.key === 'ArrowLeft') {
      prevSlide();
      stopAutoplay();
  } else if (e.key === 'ArrowRight') {
      nextSlide();
      stopAutoplay();
  }
})

Codevoorbeeld

HTML
<section 
    class="carousel" 
    aria-label="Afbeeldingen van huisdieren"
    aria-roledescription="carrousel">
    
    <!-- Bedieningselementen -->
    <div
        class="carousel-controls"
        role="group"
        aria-label="Bediening van de carrousel">
        <button 
            type="button" 
            class="previous" 
            aria-label="Vorige dia">
            <span aria-hidden="true">⬅️</span>
        </button>

        <button 
            type="button" 
            class="pause" 
            aria-label="Start de carrousel"
            aria-pressed="false">
            <span aria-hidden="true">▶️</span>
        </button>

        <button 
            type="button" 
            class="next" 
            aria-label="Volgende dia">
            <span aria-hidden="true">➡️</span>
        </button>
    </div>

    <!-- Container voor slides -->
    <ul 
        class="carousel-slides"
        aria-live="polite">
        
        <!-- Actieve slide -->
        <li 
            class="carousel-slide" 
            role="group"
            aria-roledescription="dia"
            aria-labelledby="dia1"
            aria-current="true">
            <div id="dia1">
            <span class="sr-only">Dia 1 van 3</span>
              <img src="hond.jpg" alt="Een vrolijke hond in het park">
            </div>
        </li>

        <!-- Inactieve slides -->
        <li 
            class="carousel-slide" 
            role="group"
            aria-roledescription="dia"
            aria-labelledby="dia2"
            aria-hidden="true">
            <div id="dia2">
              <span class="sr-only">Dia 2 van 3</span>
              <img id="dia2" src="kat.jpg" alt="Een slapende kat op de vensterbank">
            </div>
        </li>

        <li 
            class="carousel-slide" 
            role="group"
            aria-roledescription="dia"
            aria-labelledby="dia3"
            aria-hidden="true">
            <div id="dia3">
              <span class="sr-only">Dia 1 van 3:</span>
              <img id="dia3" src="konijn.jpg" alt="Een wit konijn dat wortels eet">
            </div>
        </li>

    </ul>

    <!-- Indicator knoppen -->
    <div
        class="carousel-indicators"
        role="group"
        aria-label="Kies een dia">
        <button 
            type="button"
            aria-label="Ga naar dia 1"
            aria-current="true"
            aria-disabled="true">
            <span class="sr-only">Dia 1</span>
        </button>

        <button 
            type="button"
            aria-label="Ga naar dia 2">
            <span class="sr-only">Dia 2</span>
        </button>

        <button 
            type="button"
            aria-label="Ga naar dia 3">
            <span class="sr-only">Dia 3</span>
        </button>
        <div class="carousel-status">
            Dia 1 van 3
        </div>
    </div>
</section>
Javascript
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // Elementen selecteren
        const carousel = document.querySelector('.carousel');
        const slides = carousel.querySelectorAll('.carousel-slide');
        const prevButton = carousel.querySelector('.previous');
        const nextButton = carousel.querySelector('.next');
        const pauseButton = carousel.querySelector('.pause');
        const indicators = carousel.querySelectorAll('.carousel-indicator');
        const statusDiv = carousel.querySelector('.carousel-status');
        
        // Variabelen
        let currentIndex = 0;
        let intervalId = null;
        let isPlaying = false;
        const totalSlides = slides.length;
        
        // Detecteer prefers-reduced-motion
        const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        
        // Functie om naar een specifieke slide te gaan
        function goToSlide(index) {
            // Vorige slide verbergen
            slides[currentIndex].setAttribute('aria-current', 'false');
            slides[currentIndex].setAttribute('aria-hidden', 'true');
            indicators[currentIndex].setAttribute('aria-current', 'false');
            
            // Nieuwe index instellen
            currentIndex = index;
            
            // Nieuwe slide tonen
            slides[currentIndex].setAttribute('aria-current', 'true');
            slides[currentIndex].setAttribute('aria-hidden', 'false');
            indicators[currentIndex].setAttribute('aria-current', 'true');
            
            // Status bijwerken
            statusDiv.textContent = `Dia ${currentIndex + 1} van ${totalSlides}`;
        }
        
        // Functie voor de volgende slide
        function nextSlide() {
            const newIndex = (currentIndex + 1) % totalSlides;
            goToSlide(newIndex);
        }
        
        // Functie voor de vorige slide
        function prevSlide() {
            const newIndex = (currentIndex - 1 + totalSlides) % totalSlides;
            goToSlide(newIndex);
        }
        
        // Functie om autoscroll te starten (niet standaard aan)
        function startAutoplay() {
            // Niet starten als gebruiker reduced motion heeft ingesteld
            if (prefersReducedMotion) {
                // Toon bericht voor screenreader gebruikers
                const motionAlert = document.createElement('div');
                motionAlert.setAttribute('aria-live', 'polite');
                motionAlert.classList.add('sr-only');
                motionAlert.textContent = 'Automatisch afspelen is uitgeschakeld vanwege uw systeemvoorkeuren voor verminderde beweging.';
                carousel.appendChild(motionAlert);
                
                // Verwijder het bericht na 5 seconden
                setTimeout(() => {
                    carousel.removeChild(motionAlert);
                }, 5000);
                
                return;
            }
            
            if (intervalId === null) {
                intervalId = setInterval(nextSlide, 5000); // Elke 5 seconden
                isPlaying = true;
                pauseButton.setAttribute('aria-pressed', 'true');
                pauseButton.setAttribute('aria-label', 'Pauzeer de carrousel');
                pauseButton.querySelector('span').textContent = '⏸️';
            }
        }
        
        // Functie om autoscroll te stoppen
        function stopAutoplay() {
            if (intervalId !== null) {
                clearInterval(intervalId);
                intervalId = null;
                isPlaying = false;
                pauseButton.setAttribute('aria-pressed', 'false');
                pauseButton.setAttribute('aria-label', 'Start de carrousel');
                pauseButton.querySelector('span').textContent = '▶️';
            }
        }
        
        // Event listeners toevoegen
        prevButton.addEventListener('click', function() {
            prevSlide();
            stopAutoplay(); // Stop autoscroll bij handmatige navigatie
        });
        
        nextButton.addEventListener('click', function() {
            nextSlide();
            stopAutoplay(); // Stop autoscroll bij handmatige navigatie
        });
        
        pauseButton.addEventListener('click', function() {
            if (isPlaying) {
                stopAutoplay();
            } else {
                startAutoplay();
            }
        });
        
        // Indicator knoppen
        indicators.forEach((indicator, index) => {
            indicator.addEventListener('click', function() {
                goToSlide(index);
                stopAutoplay(); // Stop autoscroll bij handmatige navigatie
            });
        });
        
        // Toetsenbord navigatie
        carousel.addEventListener('keydown', function(e) {
            if (e.key === 'ArrowLeft') {
                prevSlide();
                stopAutoplay();
                e.preventDefault();
            } else if (e.key === 'ArrowRight') {
                nextSlide();
                stopAutoplay();
                e.preventDefault();
            }
        });
        
        // Focus management - pauzeer als de carrousel de focus verlaat
        // carousel.addEventListener('focusout', function(e) {
        //     if (!carousel.contains(e.relatedTarget)) {
        //         stopAutoplay();
        //     }
        // });
        
        // Variabelen voor pauze bij hover
        let wasPlayingBeforeHover = false;
        
        // Stop autoscroll als de gebruiker met de muis over de carrousel hovert
        carousel.addEventListener('mouseenter', function() {
            if (isPlaying) {
                wasPlayingBeforeHover = true;
                stopAutoplay();
            } else {
                wasPlayingBeforeHover = false;
            }
        });

        // Hervat autoscroll als de muis de carrousel verlaat en het eerder speelde
        carousel.addEventListener('mouseleave', function() {
            if (wasPlayingBeforeHover && !isPlaying && !prefersReducedMotion) {
                startAutoplay();
                wasPlayingBeforeHover = false;
            }
        });
        
        // Initialisatie - zorg dat alles begint in de juiste staat
        goToSlide(0);
        
        // Standaard niet automatisch starten zoals gevraagd
        // Als je dit wilt aanpassen, kun je startAutoplay() aanroepen
        
        // Luister naar veranderingen in de prefers-reduced-motion setting
        window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
            if (e.matches && isPlaying) {
                stopAutoplay();
                
                // Informeer gebruiker
                const motionAlert = document.createElement('div');
                motionAlert.setAttribute('aria-live', 'polite');
                motionAlert.classList.add('sr-only');
                motionAlert.textContent = 'Automatisch afspelen is gestopt vanwege gewijzigde systeemvoorkeuren voor verminderde beweging.';
                carousel.appendChild(motionAlert);
                
                // Verwijder het bericht na 5 seconden
                setTimeout(() => {
                    carousel.removeChild(motionAlert);
                }, 5000);
            }
        });
    });
</script>

Demo

Toegankelijke Carrousel

Toegankelijkheidskenmerken

  1. Toetsenbordbediening
    • Alle interactieve elementen zijn focusbaar.
    • De tabvolgorde is logisch en volgt de visuele layout.
    • Pijltoetsen kunnen worden gebruikt waar nodig.
  2. Schermlezers
    • Alle bedieningselementen hebben duidelijke labels en beschrijvingen.
    • Statusupdates worden aangekondigd via aria-live.
  3. Vormgeving
    • Het bolletje van de huidige slide is groter dan de andere bolletjes.
  4. Beweging
    • De carrousel start niet automatisch.
    • De carrousel pauzeert automatisch bij handmatige navigatie.
    • De carrousel houdt rekening met de prefers-reduced-motion-instelling.

Conclusie

Een toegankelijke carrousel kost wat extra tijd, maar zorgt ervoor dat iedereen je content kan gebruiken en bekijken.

De carrousel voldoet zo aan de volgende succescriteria:

  • 1.3.1 Info en relaties
  • 1.4.1 Gebruik van kleur
  • 2.1.1 Toetsenbord
  • 2.2.2 Pauzeren, stoppen, verbergen
  • 2.4.3 Focus volgorde
  • 2.4.6 Koppen en labels
  • 2.4.7 Focus zichtbaar
  • 4.1.2 Naam, rol, waarde
  • 4.1.3 Statusberichten

Let bij het toepassen van de carrousel ook op de juiste kleuren voor:

  • 1.4.3 Contrast (minimum)
  • 1.4.11 Contrast van niet-tekstuele content

Wil je zeker weten dat jouw carrousel toegankelijk is? Test deze dan zelf eens met een schermlezer en alleen met het toetsenbord.

Heb je vragen over deze implementatie? Laat het me weten!