Skip to main content

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.


Carousel container
├── Previous button
├── Slide viewport
│ └── Active slide
├── Next button
├── Slide picker controls (optional dots/thumbnails)
└── Start or stop rotation control (if autoplay exists)
PartPurpose
ViewportDefines the visible slide area
SlideContains an image and optional caption or action
Previous/Next controlsMove sequentially through slides
Picker controlsJump directly to a known slide
Rotation controlStarts or stops automatic movement
StatusCommunicates 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.

KeyBehavior
Tab / Shift + TabMove through carousel controls normally
Enter / SpaceActivate 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​

MistakeBetter approach
Making arrow images clickableUse native buttons with meaningful labels
Ignoring an empty slide arrayRender an empty state before index calculations
Showing controls for one slideHide or disable unnecessary navigation
Allowing the index to become negative or too largeUse a tested wrap helper or finite bounds
Starting autoplay without a stop controlGive the user clear start/stop control
Leaving autoplay active during keyboard interactionStop rotation when focus enters
Announcing every automatic changeDisable live announcements during rotation
Using dots with no accessible namesLabel their destination and expose active state
Requiring swipe gesturesKeep visible keyboard-accessible controls
Loading every large image immediatelyUse responsive sources and a small preload window
Omitting image dimensionsReserve space to prevent layout shift
Leaving inactive slide controls focusableRemove inactive content from interaction
Using alt="slider" for every imageWrite 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​


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.