javascript
51 lines · 9 steps
Building an accessible auto-playing carousel
A self-contained Carousel class that cycles slides on a timer while respecting hover, tab visibility, and screen readers.
Explained by
highlit
1class Carousel {
2 constructor(root, { interval = 5000 } = {}) {
3 this.root = root;
4 this.track = root.querySelector('.carousel__track');
5 this.slides = Array.from(root.querySelectorAll('.carousel__slide'));
6 this.dots = Array.from(root.querySelectorAll('.carousel__dot'));
7 this.interval = interval;
8 this.index = 0;
9 this.timer = null;
10
11 this.root.addEventListener('mouseenter', () => this.pause());
12 this.root.addEventListener('mouseleave', () => this.play());
13 this.dots.forEach((dot, i) => dot.addEventListener('click', () => this.goTo(i)));
14 document.addEventListener('visibilitychange', () =>
15 document.hidden ? this.pause() : this.play()
16 );
17
18 this.render();
19 this.play();
20 }
21
22 render() {
23 this.track.style.transform = `translateX(-${this.index * 100}%)`;
24 this.slides.forEach((slide, i) =>
25 slide.setAttribute('aria-hidden', String(i !== this.index))
26 );
27 this.dots.forEach((dot, i) =>
28 dot.classList.toggle('is-active', i === this.index)
29 );
30 }
31
32 goTo(index) {
33 this.index = (index + this.slides.length) % this.slides.length;
34 this.render();
35 this.play();
36 }
37
38 next() {
39 this.goTo(this.index + 1);
40 }
41
42 play() {
43 this.pause();
44 this.timer = setInterval(() => this.next(), this.interval);
45 }
46
47 pause() {
48 clearInterval(this.timer);
49 this.timer = null;
50 }
51}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Storing a single index and re-deriving the DOM from it keeps UI state predictable.
- 2Pausing timers on hover and tab-hidden avoids wasted work and jarring jumps.
- 3Toggling aria-hidden alongside visual state keeps assistive tech in sync with what's shown.
Related explainers
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-accessible-auto-playing-carousel-explained-javascript-e60f/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.