Carrousel

Carousels zijn populair maar vaak een toegankelijkheidsnachtmerrie. Bewegende content, automatisch afspelen, onduidelijke navigatie: Het kan allemaal fout gaan. Maar het kan ook goed!

De belangrijkste regel: gebruikers moeten de carousel kunnen pauzeren. Bewegende content is voor veel mensen storend of zelfs onmogelijk te lezen. Een pauzeknop is dus een must.

Elke slide moet:

  • Een duidelijk label hebben (Slide 1 van 5)
  • Met het toetsenbord bereikbaar zijn
  • Aangeven welke slide actief is

Technische opbouw

  • Geef de container de rol section
  • Geef de container een naam (via aria-labelledby of aria-label)
  • Geef de container een rolbeschrijving aria-roledescription="carousel"
  • Groepeer elke slide met role="group"
  • Geef elke slide een naam (via aria-labelledby of aria-label)
  • Geef elke slide een rolbeschrijving aria-roledescription="slide"
  • Voor huidige slide:
    • Geef de slide de waarde aria-current="slide"
  • Maak elke knop op een <button>-element
  • Geef elke knop een naam (via aria-labelledby of aria-label)

Voorbeeld carrousel

HTML
<section class="carousel" role="section" aria-label="Productfoto's" aria-roledescription="carousel">
  <div class="carousel-slides" id="carousel-slides">
    <div class="carousel-slide active" role="group" aria-current="slide" aria-label="Slide 1 van 3" aria-roledescription="slide">
      <h3>Slide 1</h3>
      <p>Dit is de eerste slide met belangrijke informatie.</p>
    </div>
    <div class="carousel-slide" role="group" aria-label="Slide 2 van 3" aria-roledescription="slide">
      <h3>Slide 2</h3>
      <p>Dit is de tweede slide met meer details.</p>
    </div>
    <div class="carousel-slide" role="group" aria-label="Slide 3 van 3" aria-roledescription="slide">
      <h3>Slide 3</h3>
      <p>Dit is de derde en laatste slide.</p>
    </div>
  </div>
  
  <div class="carousel-controls">
    <button class="carousel-prev" aria-controls="carousel-slides" aria-label="Vorige slide">
      Vorige
    </button>
    <button class="carousel-pause" aria-label="Pauzeer autoplay">
      Pauzeer
    </button>
    <button class="carousel-next" aria-controls="carousel-slides" aria-label="Volgende slide">
      Volgende
    </button>
  </div>
  
  <div class="carousel-nav" role="group" aria-label="Slide knoppen">
    <button class="carousel-indicator active" aria-controls="carousel-slides" aria-label="Ga naar slide 1"></button>
    <button class="carousel-indicator" aria-controls="carousel-slides" aria-label="Ga naar slide 2"></button>
    <button class="carousel-indicator" aria-controls="carousel-slides" aria-label="Ga naar slide 3"></button>
  </div>
</section>
CSS
/* Carrousel stijlen */
.carousel {
  position: relative;
  max-width: 600px;
  margin: 0 auto;
}

.carousel-slides {
  position: relative;
  overflow: hidden;
  border-radius: 4px;
}

.carousel-slide {
  padding: 2rem;
  background: #f0f8ff;
  border: 1px solid black;
  text-align: center;
  display: none;
  border-radius: 4px;
}

.carousel-slide.active {
  display: block;
}

/* Belangrijke toegankelijkheidsregel: verborgen slides niet focusbaar */
.carousel-slide:not(.active) * {
  visibility: hidden;
}

.carousel-controls {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}

.carousel-controls button {
  padding: 1rem;
  background: black;
  color: white;
  border: none;
  font-size: 0.9rem;
}

.carousel-controls button:hover,
.carousel-controls button:focus {
  color: black;
  background: white;
}

.carousel-nav {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 1rem;
}

.carousel-indicator {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid black;
}

.carousel-indicator:hover,
.carousel-indicator:focus {
  outline: 2px solid #ff6600;
  outline-offset: 2px;
}

.carousel-indicator.active {
  background: #0066cc;
}

/* Screen reader only content */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
JavaScript
// Carrousel functionaliteit
class Carousel {
  constructor() {
    this.carousel = document.querySelector('.carousel');
    this.slides = document.querySelectorAll('.carousel-slide');
    this.indicators = document.querySelectorAll('.carousel-indicator');
    this.prevBtn = document.querySelector('.carousel-prev');
    this.nextBtn = document.querySelector('.carousel-next');
    this.pauseBtn = document.querySelector('.carousel-pause');
    this.status = document.getElementById('carousel-status');
    this.currentSlide = 0;
    this.isPlaying = false;
    this.autoplayInterval = null;

    this.init();
  }

  init() {
    // Event listeners voor knoppen
    this.prevBtn.addEventListener('click', () => this.prevSlide());
    this.nextBtn.addEventListener('click', () => this.nextSlide());
    this.pauseBtn.addEventListener('click', () => this.toggleAutoplay());

    // Event listeners voor indicatoren
    this.indicators.forEach((indicator, index) => {
      indicator.addEventListener('click', () => this.goToSlide(index));
    });

    // Toetsenbord navigatie
    this.carousel.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowLeft') {
        e.preventDefault();
        this.prevSlide();
      } else if (e.key === 'ArrowRight') {
        e.preventDefault();
        this.nextSlide();
      }
    });

    // Pauzeer bij mouse hover
    this.carousel.addEventListener('mouseenter', () => this.pause());
    this.carousel.addEventListener('mouseleave', () => this.resume());

    // Pauzeer bij focus
    this.carousel.addEventListener('focusin', () => this.pause());
    this.carousel.addEventListener('focusout', () => this.resume());

    this.updateStatus();
  }

  goToSlide(index) {
    // Verwijder actieve status van huidige slide
    this.slides[this.currentSlide].classList.remove('active');
    this.slides[this.currentSlide].removeAttribute('aria-current');
    this.indicators[this.currentSlide].classList.remove('active');

    // Stel nieuwe slide in
    this.currentSlide = index;

    // Voeg actieve status toe aan nieuwe slide
    this.slides[this.currentSlide].classList.add('active');
    this.slides[this.currentSlide].setAttribute('aria-current', 'slide');
    this.indicators[this.currentSlide].classList.add('active');

    this.updateStatus();
  }

  nextSlide() {
    const next = (this.currentSlide + 1) % this.slides.length;
    this.goToSlide(next);
  }

  prevSlide() {
    const prev = (this.currentSlide - 1 + this.slides.length) % this.slides.length;
    this.goToSlide(prev);
  }

  toggleAutoplay() {
    if (this.isPlaying) {
      this.stopAutoplay();
    } else {
      this.startAutoplay();
    }
  }

  startAutoplay() {
    this.isPlaying = true;
    this.pauseBtn.textContent = 'Pauzeer';
    this.pauseBtn.setAttribute('aria-label', 'Pauzeer autoplay');
    this.autoplayInterval = setInterval(() => {
      this.nextSlide();
    }, 4000);
  }

  stopAutoplay() {
    this.isPlaying = false;
    this.pauseBtn.textContent = 'Speel af';
    this.pauseBtn.setAttribute('aria-label', 'Start autoplay');
    if (this.autoplayInterval) {
      clearInterval(this.autoplayInterval);
      this.autoplayInterval = null;
    }
  }

  pause() {
    if (this.autoplayInterval) {
      clearInterval(this.autoplayInterval);
    }
  }

  resume() {
    if (this.isPlaying && !this.autoplayInterval) {
      this.autoplayInterval = setInterval(() => {
        this.nextSlide();
      }, 4000);
    }
  }
}

// Start de carrousel
document.addEventListener('DOMContentLoaded', () => {
  new Carousel();
});

Toetsenbordbediening

  • Tab:
    • Gaat naar het volgende focusbare element in de carrousel
  • Shift + Tab:
    • Gaat naar het vorige focusbare element in de carrousel

Optionele toetsen

  • Pijltjestoetsen:
    • Pijl recht: gaat naar de volgende slide
    • Pijl links: gaat naar de vorige slide

Focus management

  • Zet rotation control altijd vóór de slides in tab-volgorde
  • De focus mag niet op verborgen slides terechtkomen

Checklist

Component

  • (4.1.2) De carrousel-container moet de rol section hebben
  • (4.1.2) De carrousel-container moet een toegankelijke naam hebben
    • (2.4.6) De toegankelijke naam moet beschrijven wat er in de carrousel staat
  • (4.1.2) De carrousel-container zou aria-roledescription="carousel" moeten hebben
  • (4.1.2) Elke slide moet role="group" hebben
  • (4.1.2) Elke slide moet een toegankelijke naam hebben
    • (2.4.6) De toegankelijke naam zou de positie-informatie moeten hebben (“1 van 6”, “2 van 6”, enz.)
  • (4.1.2) Elke slide zou aria-roledescription="slide" moeten hebben
  • (4.1.2) De huidige slide moet aria-current="slide" hebben
  • (4.1.2) Alle knoppen moeten een <button>-element zijn
  • (4.1.2) Alle knoppen moeten een toegankelijke naam hebben
    • (2.4.6) De toegankelijke naam moet beschrijven wat het resultaat van de actie is
  • (4.1.2) Navigatie-knoppen moeten aria-controls hebben met het id van de slides-container
  • (4.1.3) Bij automatische rotatie moet de slides-container aria-live="off" hebben
  • (4.1.3) Bij handmatige navigatie moet de slides-container aria-live="polite" hebben

Bronnen