Build Your Portfolio
Beginner

JavaScript: The Slider and the Credits Panel

Make the page respond to people. Build a slider that rotates on its own, jumps when you click a dot, and politely pauses while someone is reading. Then a pop-up panel that closes the three ways people expect: the button, the Escape key, or a click anywhere else.

JavaScript: The Slider and the Credits Panel

The clock in the last lesson changes on its own. This lesson is about changes the visitor causes: clicking a dot, pressing a key, tapping somewhere else on the screen.

Both features here are small. But they're where beginner JavaScript usually goes wrong in ways that annoy real people: sliders that rush on while you're still reading, pop-ups you can't close without a mouse. We'll build them the polite way from the start.

The slider: one number, one class

Here's the whole idea in one sentence: a variable remembers which slide is showing, and a class marks it. CSS already knows how to fade slides in and out based on the is-active class (lesson 6). JavaScript's only job is to move that class.

Animation: three slide cards stacked in one grid cell; as current goes 0, 1, 2, the matching card comes to the front with the is-active class while the others are hidden, and the matching dot lights up
JavaScript moves the class. CSS does all the fading.
const slides = document.querySelectorAll(".slide");
const dots = document.querySelectorAll(".dot");
const hero = document.querySelector(".hero");
let current = 0;
let timer = null;

querySelectorAll returns every match, as a list. slides[0] is the first slide, slides[2] the third. current uses let because it will change; it starts at 0, matching the slide that has is-active in the HTML.

Showing a slide

function showSlide(index) {
  slides[current].classList.remove("is-active");
  dots[current].classList.remove("is-active");
  dots[current].removeAttribute("aria-current");

  current = index;

  slides[current].classList.add("is-active");
  dots[current].classList.add("is-active");
  dots[current].setAttribute("aria-current", "true");
}

Three steps: turn off the old one, update current, turn on the new one. classList.add / remove change an element's classes without touching its other classes.

We also move aria-current onto the active dot. Sighted users see the bright bar; screen reader users hear "Slide 2, current". Same information, two senses.

Moving forward forever

function nextSlide() {
  showSlide((current + 1) % slides.length); // after the last one, wrap to 0
}

% (remainder) makes the count wrap around: with 3 slides, (0+1) % 3 is 1, (1+1) % 3 is 2, and (2+1) % 3 is 0. Back to the start, with no if needed. You'll use this pattern for carousels, playlists and anything else that loops.


Autoplay, done politely

function startAutoplay() {
  // People who asked their system for less motion get no autoplay.
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
  stopAutoplay();
  timer = setInterval(nextSlide, 5000);
}

function stopAutoplay() {
  clearInterval(timer);
}
  • setInterval returns an id. We keep it in timer so clearInterval(timer) can stop it later.
  • startAutoplay calls stopAutoplay first, so you can never accidentally run two timers at once. Two timers would make the slides race.
  • prefers-reduced-motion is a system setting people turn on when movement makes them dizzy or distracts them. matchMedia lets JavaScript read it. If it's on, the slider simply doesn't move by itself. The dots still work.

Then the events:

dots.forEach((dot, index) => {
  dot.addEventListener("click", () => {
    showSlide(index);
    startAutoplay(); // restart the 5 seconds after a manual choice
  });
});

// Pause while someone is reading or using the keyboard inside the hero.
hero.addEventListener("mouseenter", stopAutoplay);
hero.addEventListener("mouseleave", startAutoplay);
hero.addEventListener("focusin", stopAutoplay);
hero.addEventListener("focusout", startAutoplay);

startAutoplay();
  • addEventListener(event, fn) runs fn whenever that event happens on the element.
  • forEach((dot, index) => …) loops over the dots and gives us each one's position, so dot 2 knows to show slide 2.
  • Restarting the timer after a click gives the visitor a full five seconds with the slide they chose.
  • mouseenter / mouseleave pause while the pointer is over the hero, so a slide never changes under someone who's reading it.
  • focusin / focusout do the same for keyboard users. When someone Tabs to a dot, the slider holds still.

Because the dots are real <button>s (lesson 2), pressing Enter or Space on one fires the same click event. Keyboard support came free with the right HTML.


The credits panel

The ••• button opens a panel listing every artist. People expect a pop-up like this to close in three ways, so it should:

The ••• click toggles the panel; three paths, clicking ••• again, pressing Escape, or clicking outside, all lead to setCreditsOpen(false)
Three ways out, one function that does the closing.
const creditsToggle = document.querySelector(".credits-toggle");
const creditsPanel = document.querySelector("#credits-panel");

function setCreditsOpen(open) {
  creditsPanel.hidden = !open;
  creditsToggle.setAttribute("aria-expanded", String(open));
}

One function owns the open/closed state, and everything else calls it. That way the panel and the button can never disagree:

  • hidden is a real HTML attribute. The browser hides elements that have it, and screen readers skip them. Setting creditsPanel.hidden = true adds it; false removes it.
  • aria-expanded tells screen readers whether the button's panel is open. It's the same pattern your browser's own menus use.

Now the three ways to open and close it:

creditsToggle.addEventListener("click", () => {
  setCreditsOpen(creditsPanel.hidden); // hidden means closed, so open it
});

// Close it with Escape, or by clicking anywhere else.
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") setCreditsOpen(false);
});

document.addEventListener("click", (event) => {
  if (!event.target.closest(".credits")) setCreditsOpen(false);
});
  • The toggle passes creditsPanel.hidden: if it's hidden (closed), that's true, so we open it, and the other way round. One line handles both directions.
  • keydown on document hears every key press on the page. event.key names the key.
  • The outside click: every click on the page reaches document. event.target is the element that was actually clicked, and closest(".credits") walks up from it looking for the credits bar. If there isn't one, the click was outside, so we close. Clicks inside the panel, like on an artist's link, keep it open.

Checkpoint: try to break it

Reload, then test like a tester:

  1. Wait five seconds. The slide changes. Hover over the hero: it stops.
  2. Click the third bar. Slide 3 shows, and the bar lights up.
  3. Press Tab until a bar is focused, then press Enter. It works with no mouse.
  4. Click •••. The panel opens. Press Escape: it closes. Open it again, then click the hero: it closes.
  5. Turn on Reduce motion in your system settings and reload. The slider stays still until you click.

If something looks wrong

  • Slides change twice as fast after a while. Two timers are running. Make sure startAutoplay calls stopAutoplay() first.
  • The panel closes the moment it opens. The outside-click check is catching the button's own click. Check the button is inside .credits, so closest(".credits") finds it.
  • Clicking a dot does nothing. In the Console, run document.querySelectorAll(".dot").length. If it's 0, the class names in the HTML don't match.

What to remember

  • Keep state in a variable (current) and make the page match it by moving a class.
  • addEventListener connects events (click, keydown, focus) to your functions.
  • % wraps a counter around, which is perfect for anything that loops.
  • Respect prefers-reduced-motion, pause autoplay while people read, and keep everything keyboard-reachable.
  • One function should own each piece of state (setCreditsOpen), and everything else should call it.

Last lesson: make it work on phones, check it's accessible, and put it online: Responsive, Accessible, and Online.