Image Slider
An image slider, also called a carousel or slideshow, displays one slide from a collection and lets the user move between them.
Slides array + active index
↓
Render active slide
↓
Previous / Next / Picker / Swipe
↓
Update active index
Common examples include product galleries, marketing banners, portfolio galleries, testimonials, and featured media.
Core idea: The active slide is state; navigation actions transform that state within valid boundaries.
1. Carousel Anatomy
Carousel container
├── Previous button
├── Slide viewport
│ └── Active slide
├── Next button
├── Slide picker controls (optional dots/thumbnails)
└── Start or stop rotation control (if autoplay exists)
| Part | Purpose |
|---|---|
| Viewport | Defines the visible slide area |
| Slide | Contains an image and optional caption or action |
| Previous/Next controls | Move sequentially through slides |
| Picker controls | Jump directly to a known slide |
| Rotation control | Starts or stops automatic movement |
| Status | Communicates position, such as “2 of 5” |
2. Model Slides as Data
Use objects rather than an array of bare URLs. Each slide then carries its own identity, dimensions, accessible text, and responsive sources.
const slides = [
{
id: "mountains",
src: "/images/mountains-1280.jpg",
srcSet: [
"/images/mountains-640.jpg 640w",
"/images/mountains-1280.jpg 1280w",
].join(", "),
alt: "Snow-covered mountains beneath a clear blue sky",
width: 1280,
height: 720,
caption: "Himalayan morning",
},
{
id: "forest",
src: "/images/forest-1280.jpg",
alt: "A path passing through a green forest",
width: 1280,
height: 720,
caption: "Forest trail",
},
];
Stable IDs help with React keys, analytics, deep linking, and updates if the slide order changes.
3. Active Index State
The simplest slider stores the position of the visible slide.
const [activeIndex, setActiveIndex] = useState(0);
const activeSlide = slides[activeIndex];
activeIndex = 0 → first slide
activeIndex = 1 → second slide
activeIndex = 2 → third slide
Store the smallest state needed. The active slide object is derived from the array and index, so it does not need separate state.
4. Circular Navigation with Modulo
A circular slider wraps from the last slide to the first and from the first slide to the last.
function wrapIndex(index, length) {
return ((index % length) + length) % length;
}
The second modulo handles negative values correctly.
function showNext() {
setActiveIndex(current => wrapIndex(current + 1, slides.length));
}
function showPrevious() {
setActiveIndex(current => wrapIndex(current - 1, slides.length));
}
Example with five slides:
Next from index 4 → wrapIndex(5, 5) → 0
Previous from index 0 → wrapIndex(-1, 5) → 4
Use functional state updates because the next index depends on the current one.
5. Handle Empty and Single-Slide Collections
Modulo by zero produces an invalid value, and navigation around one slide serves no purpose.
if (slides.length === 0) {
return <p>No images are available.</p>;
}
const canNavigate = slides.length > 1;
When canNavigate is false:
- hide or disable previous and next controls;
- hide picker controls;
- do not create an autoplay timer;
- render the single image normally.
If the slides prop can shrink, clamp or reset the index:
useEffect(() => {
setActiveIndex(current => Math.min(current, slides.length - 1));
}, [slides.length]);
The empty collection must be handled before using the resulting index.
6. Accessible React Implementation
This example renders one active slide and provides manual circular navigation.
import { useEffect, useState } from "react";
export default function ImageSlider({
slides,
label = "Featured images",
}) {
const [activeIndex, setActiveIndex] = useState(0);
const count = slides.length;
useEffect(() => {
if (count > 0) {
setActiveIndex(current => Math.min(current, count - 1));
}
}, [count]);
if (count === 0) {
return <p>No images are available.</p>;
}
const slide = slides[activeIndex];
const canNavigate = count > 1;
function goTo(index) {
setActiveIndex(wrapIndex(index, count));
}
return (
<section
className="carousel"
role="region"
aria-roledescription="carousel"
aria-label={label}
>
<div className="carousel__viewport" aria-live="polite">
<figure
className="carousel__slide"
role="group"
aria-roledescription="slide"
aria-label={`${activeIndex + 1} of ${count}`}
>
<img
src={slide.src}
srcSet={slide.srcSet}
sizes="(max-width: 48rem) 100vw, 48rem"
width={slide.width}
height={slide.height}
alt={slide.alt}
/>
{slide.caption && <figcaption>{slide.caption}</figcaption>}
</figure>
</div>
{canNavigate && (
<div className="carousel__controls">
<button
type="button"
onClick={() => goTo(activeIndex - 1)}
aria-label="Previous slide"
>
←
</button>
<span aria-hidden="true">
{activeIndex + 1} / {count}
</span>
<button
type="button"
onClick={() => goTo(activeIndex + 1)}
aria-label="Next slide"
>
→
</button>
</div>
)}
{canNavigate && (
<div className="carousel__pickers" aria-label="Choose slide">
{slides.map((item, index) => (
<button
key={item.id}
type="button"
aria-label={`Show slide ${index + 1}: ${item.caption ?? item.alt}`}
aria-current={index === activeIndex ? "true" : undefined}
onClick={() => goTo(index)}
>
<span aria-hidden="true">●</span>
</button>
))}
</div>
)}
</section>
);
}
function wrapIndex(index, length) {
return ((index % length) + length) % length;
}
The arrows are native buttons rather than clickable images. This provides focus, keyboard activation, button semantics, and a larger controllable target.
7. Finite vs Circular Behavior
Not every slider should loop.
Circular
1 → 2 → 3 → 1
Useful for galleries or promotional slides where the collection has no meaningful end.
Finite
1 → 2 → 3 → stop
Useful when order and completion matter. Disable the previous button on the first slide and the next button on the last slide.
<button disabled={activeIndex === 0}>Previous</button>
<button disabled={activeIndex === slides.length - 1}>Next</button>
Choose the behavior explicitly; do not add looping only because modulo makes it easy.
8. Autoplay Requires User Control
Autoplay is not simply a setInterval.
If slides rotate automatically:
- provide a clearly labeled start/stop control;
- stop rotation when keyboard focus enters the carousel;
- pause while the pointer is over content when appropriate;
- do not resume after focus without an explicit user action;
- pause when the page becomes hidden;
- respect
prefers-reduced-motion; - give users enough time to read each slide.
useEffect(() => {
if (!isPlaying || slides.length < 2) return;
const timerId = window.setInterval(() => {
setActiveIndex(current =>
wrapIndex(current + 1, slides.length)
);
}, intervalMs);
return () => window.clearInterval(timerId);
}, [isPlaying, intervalMs, slides.length]);
Always clear the timer when its dependencies change or the component unmounts.
Place the rotation control before rotating content in the keyboard order:
<button
type="button"
onClick={() => setIsPlaying(value => !value)}
>
{isPlaying ? "Stop slide rotation" : "Start slide rotation"}
</button>
Manual navigation should reset or pause the timer so the next automatic change does not occur immediately after the user's action.
9. Keyboard Behavior
At minimum, every visible control must work through normal button interaction.
| Key | Behavior |
|---|---|
Tab / Shift + Tab | Move through carousel controls normally |
Enter / Space | Activate the focused button |
Left and Right Arrow navigation may be added when it matches the chosen pattern and does not interfere with page scrolling or controls inside the slide.
Do not trap keyboard focus inside the carousel. Activating previous or next should normally keep focus on that control so the user can repeat the action.
10. Slide Pickers and Thumbnails
Dots must not rely on visual appearance alone.
<button
aria-label="Show slide 3: Forest trail"
aria-current="true"
>
<span aria-hidden="true">●</span>
</button>
For a small collection, buttons are straightforward. Many picker buttons create many tab stops, so thumbnails may be better presented as a scrollable list or the picker may be omitted for a large collection.
The active picker should differ by more than color—for example, shape, border, size, or an explicit label.
11. Swipe and Drag Support
Touch gestures are an enhancement, not the only navigation method. Keep visible previous and next controls.
A gesture usually tracks:
Pointer down → remember start position
Pointer move → measure horizontal and vertical distance
Pointer up → compare with movement threshold
const horizontalDistance = endX - startX;
const verticalDistance = endY - startY;
if (
Math.abs(horizontalDistance) > 50 &&
Math.abs(horizontalDistance) > Math.abs(verticalDistance)
) {
horizontalDistance < 0 ? showNext() : showPrevious();
}
Important details:
- distinguish horizontal intent from vertical page scrolling;
- use Pointer Events so mouse, pen, and touch share one model;
- set pointer capture while dragging if needed;
- handle cancellation;
- avoid changing slides for tiny accidental movements;
- support right-to-left interface expectations intentionally.
12. Animation Strategies
Replace the Active Slide
Only the current slide is mounted.
Simple state and accessibility
Lower DOM size
Limited sliding transition options
Translate a Track
All slides are arranged horizontally and a track is transformed.
.carousel__track {
display: flex;
transform: translateX(calc(var(--active-index) * -100%));
transition: transform 300ms ease;
}
@media (prefers-reduced-motion: reduce) {
.carousel__track {
transition: none;
}
}
Track-based animation requires extra care:
- inactive slides must not expose interactive descendants to keyboard users;
- hidden slides should not all be announced;
- width changes must not shift the track unexpectedly;
- cloned slides used for seamless loops must be hidden from assistive technology and must not duplicate IDs.
Animate transform and opacity when possible rather than properties that repeatedly trigger layout.
13. Responsive Images and Layout Stability
The slider should not download a desktop-sized image for every mobile user.
<img
src="product-1280.jpg"
srcset="product-640.jpg 640w, product-1280.jpg 1280w"
sizes="(max-width: 48rem) 100vw, 48rem"
width="1280"
height="720"
alt="Side view of the blue running shoe"
/>
srcset and sizes let the browser choose an appropriate source. Explicit dimensions reserve the aspect ratio and reduce layout shift.
Use object-fit according to the content:
.carousel__slide img {
display: block;
inline-size: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
cover may crop meaningful content. Product images or diagrams may require contain instead.
14. Image Loading Strategy
The first visible slide may be important for Largest Contentful Paint, while later slides can usually wait.
Active initial image → load eagerly when it is critical
Next likely image → optionally preload
Distant images → lazy-load
Avoid both extremes:
- loading every full-resolution image immediately wastes bandwidth;
- lazy-loading only when a slide becomes active may show an empty delay.
Preload the next one or two likely images after the initial page is usable. Use measured user behavior and network cost to choose the window.
Provide a stable placeholder or background while an image loads, and handle broken image URLs without collapsing the component.
15. Alternative Text and Captions
Alt text describes the purpose of the image in its current context.
Informative product image → describe the useful visual information
Linked promotional image → describe the link's purpose
Pure decoration → alt=""
Visible caption repeats it → avoid unnecessary duplication
Do not use generic text such as alt="slider", alt="image", or filenames. The next and previous buttons describe their actions; their arrow icons should be hidden from assistive technology.
If a slide contains a heading, paragraph, and call-to-action, treat it as structured content rather than putting all text inside the image.
16. Visibility and Announcements
Manual slide changes can use a polite live region so a screen reader receives an update. Automatic rotation should not repeatedly interrupt users with announcements.
One strategy is:
Autoplay running → aria-live="off"
Autoplay stopped → aria-live="polite"
Each visible slide should have a useful accessible name, such as its heading or “3 of 5.” Avoid placing focus automatically on a newly displayed slide; focus usually remains on the control the user activated.
17. State Ownership
Uncontrolled Slider
The slider owns activeIndex and exposes initial configuration.
<ImageSlider slides={slides} defaultIndex={0} />
Controlled Slider
The parent owns the active value.
<ImageSlider
slides={slides}
activeIndex={activeIndex}
onActiveIndexChange={setActiveIndex}
/>
Controlled state is useful when:
- slide selection appears in the URL;
- thumbnails and another gallery view must stay synchronized;
- analytics or product state depends on the selected media;
- an external control changes the slide.
Prefer a stable slide ID over an index when the collection can be reordered dynamically.
18. Common Mistakes
| Mistake | Better approach |
|---|---|
| Making arrow images clickable | Use native buttons with meaningful labels |
| Ignoring an empty slide array | Render an empty state before index calculations |
| Showing controls for one slide | Hide or disable unnecessary navigation |
| Allowing the index to become negative or too large | Use a tested wrap helper or finite bounds |
| Starting autoplay without a stop control | Give the user clear start/stop control |
| Leaving autoplay active during keyboard interaction | Stop rotation when focus enters |
| Announcing every automatic change | Disable live announcements during rotation |
| Using dots with no accessible names | Label their destination and expose active state |
| Requiring swipe gestures | Keep visible keyboard-accessible controls |
| Loading every large image immediately | Use responsive sources and a small preload window |
| Omitting image dimensions | Reserve space to prevent layout shift |
| Leaving inactive slide controls focusable | Remove inactive content from interaction |
Using alt="slider" for every image | Write contextual alternative text |
19. Testing Checklist
State and Navigation
- Next and previous move to the correct slide.
- Circular navigation wraps in both directions.
- Finite navigation disables the correct boundary control.
- Direct picker selection opens the requested slide.
- Empty and single-slide collections render safely.
- Changing the slide array keeps selection valid.
Autoplay and Timing
- Only one timer exists.
- The timer is cleared on unmount and configuration changes.
- Autoplay stops for focus, explicit pause, and reduced motion.
- Manual interaction does not cause an immediate unexpected rotation.
- Hidden browser tabs do not waste unnecessary work.
Accessibility
- Every control is reachable and usable by keyboard.
- Focus remains visible and predictable.
- Buttons have meaningful accessible names.
- Position and active picker state are communicated.
- Inactive slides are not focusable or repeatedly announced.
- Content remains usable at zoom and narrow viewport sizes.
Performance and Gestures
- Appropriate image sources load at common viewport sizes.
- Dimensions prevent layout shift.
- Swipes do not block ordinary vertical scrolling.
- Slow and failed images have usable feedback.
- Animation remains smooth and respects reduced motion.
Official References
- W3C WAI: Carousel Pattern
- W3C WAI: Carousels Tutorial
- W3C WAI: Carousel Styling
- MDN:
<img>Element and Responsive Images
Summary
An image slider is active-slide state plus navigation, made production-ready through user-controlled motion, accessible controls, responsive media, and careful edge-case handling.
Think of the slider as a small state machine:
State
├── active slide
└── rotation running or stopped
Events
├── previous
├── next
├── choose slide
├── timer tick
├── swipe
├── focus enters
└── user stops rotation
Output
├── one understandable active slide
├── synchronized controls and status
└── correctly loaded responsive image
The modulo expression solves index wrapping. The real component design lies in controlling movement, focus, loading, visibility, and user choice.