Start with a deliberate interaction model

A carousel is useful when several related items need to share a compact area, but it should not make visitors race to read them. For this example, the active slide changes only when someone presses Previous or Next. There is no timer, and reaching either end disables the control that cannot move farther. This makes the first and last positions clear instead of unexpectedly wrapping around.

The example assumes a modern browser, ordinary HTML, CSS, and plain JavaScript, with no framework or carousel library. In the page, create a container with the id slides. Give each direct child slide the class slide. Place two buttons outside that container with the ids previous and next and visible text reading Previous and Next. Add an element with the id position near the buttons to show the current slide number. Keep these controls outside the slides so they remain available when a slide is hidden.

Keep one slide visible

The carousel needs only one piece of changing state: the index of the active slide. Collect the slide elements in their document order, start at index zero, and use a render function to reflect that index in the page. The essential setup is: const slides = Array.from(document.querySelectorAll('#slides > .slide')); const previous = document.getElementById('previous'); const next = document.getElementById('next'); const position = document.getElementById('position'); let index = 0;

Inside render, update every slide and both buttons: slides.forEach((slide, i) => { slide.hidden = i !== index; }); previous.disabled = index === 0; next.disabled = index === slides.length - 1; position.textContent = 'Slide ' + (index + 1) + ' of ' + slides.length; Call render() once after defining it. The hidden property keeps inactive slides out of normal display and keyboard navigation. Check that your CSS does not override that behavior with a conflicting display rule.

Handle an empty container before rendering, because an index of zero cannot identify a slide when none exist. For example, after collecting the elements, disable both buttons and set the position text to 'No slides' if slides.length is zero. Only run the normal render and event setup when at least one slide exists.

Connect the buttons without a timer

Attach a click listener to each button. The Previous listener can use: if (index > 0) { index--; render(); }. The Next listener can use: if (index < slides.length - 1) { index++; render(); }. Register those listeners with previous.addEventListener('click', ...) and next.addEventListener('click', ...). These bounds match the disabled states shown by render, so a click cannot move to an invalid index even if the state changes unexpectedly.

Native buttons already work with a mouse, touch, Enter, and Space. Avoid replacing them with clickable generic containers that need extra keyboard behavior. You also do not need to capture arrow keys across the page: doing so can interfere with reading or with controls inside a slide. If you later add swipe gestures or keyboard shortcuts, keep the visible buttons as an equivalent way to operate the carousel.

No interval or timeout belongs in this version. Removing forced auto-advance is more than a courtesy for slow readers: it also means a person can pause to compare details, operate a link, or return to an earlier item without the content moving underneath them.

Make the content and status understandable

Each slide should make sense on its own. Give images appropriate alternative text, use descriptive link text, and avoid making the slide's position its only identifier. A short heading such as a product name or feature title helps visitors recognize what changed. The position element can provide a useful visual cue: 'Slide 2 of 4' tells people both where they are and how much remains.

If you want that status announced to screen readers after a button press, make the position element a polite live region in the page markup. Update its text through render rather than repeatedly moving keyboard focus to the slide. Focus should normally stay on the button the visitor pressed, ready for another action. Because the buttons sit outside the slides, hiding the previous slide will not hide the focused control.

Be restrained with announcements. A short position update is usually easier to follow than having the entire new slide read aloud. The visible slide remains available through normal navigation when someone wants its full content.

Test the edges before adding extras

Try the carousel with one slide, several slides, and no slides. With one slide, both buttons should be disabled and the position should read 'Slide 1 of 1'. With several, Previous should be disabled at the start, Next at the end, and only one slide should be visible at a time. Tab through the page after changing slides to confirm that links or buttons in hidden slides are not reached.

Finally, test without using a mouse: Tab to a control, activate it with Enter or Space, and check that focus stays predictable. Review the page at a narrow width and with longer slide content, where controls can become separated from the item they operate. A manual carousel can stay simple; clear controls and steady content matter more than animation.

Back to category ↗