Building a Complete React Image Slider Component

Below is a detailed, step-by-step explanation of your ImageSlider component. I include the full React component code and the full CSS, then I explain every part: props, state, fetch logic, rendering, event handlers, edge cases, and possible improvements. Read straight through or jump to the part you need.

Full Code

// ImageSlider.jsx
import { useEffect, useState } from 'react'
import { BsArrowLeftCircleFill, BsArrowRightCircleFill } from 'react-icons/bs'
import './styles.css'

export default function ImageSlider ({ url, limit = 5, page = 1 }) {
const [images, setImages] = useState([])
const [currentSlide, setCurrentSlide] = useState(0)
const [errorMsg, setErrorMsg] = useState(null)
const [loading, setLoading] = useState(false)
async function fetchImages (getUrl) {
//console.log(getUrl);
try {
setLoading(true)
const response = await fetch(`${getUrl}?page=${page}&limit=${limit}`)
const data = await response.json()
//console.log(data);
if (data) {
setImages(data)
setLoading(false)
}
} catch (e) {
setErrorMsg(e.message)
setLoading(false)
}
}
function handlePrevious () {
setCurrentSlide(currentSlide === 0 ? images.length - 1 : currentSlide - 1)
}
function handleNext () {
setCurrentSlide(currentSlide === images.length - 1 ? 0 : currentSlide + 1)
}
useEffect(() => {
if (url !== '') fetchImages(url)
}, [url])
console.log(images)
if (loading) {
return <div>Loading data ! Please wait</div>
}
if (errorMsg !== null) {
return <div>Error occured ! {errorMsg}</div>
}
console.log(images.length)
return (
<div className='container'>
<BsArrowLeftCircleFill
onClick={handlePrevious}
className='arrow arrow-left'
/>
{images && images.length
? images.map((imageItem, index) => (
<img
key={imageItem.id}
alt={imageItem.download_url}
src={imageItem.download_url}
className={
currentSlide === index
? 'current-image'
: 'current-image hide-current-image'
}
/>
))
: null}
<BsArrowRightCircleFill
onClick={handleNext}
className='arrow arrow-right'
/>
<span className='circle-indicators'>
{images && images.length
? images.map((_, index) => (
<button
key={index}
className=
{currentSlide === index
? 'current-indicator'
: 'current-indicator inactive-indicator'}
onClick={() => setCurrentSlide(index)}
></button>
))
: null}
</span>
</div>
)
}

Full code— CSS

/* styles.css */
.container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 600px;
height: 450px;
}

.current-image{
border-radius: 0.5rem;
box-shadow: 0px 0px 7px #666;
width: 100%;
height: 100%;
}
.hide-current-image{
display: none;
}
.arrow{
position: absolute;
width: 2rem;
height: 2rem;
color: #fff;
filter: drop-shadow(0px 0px 5px #555);
}
.arrow-left{
left:1rem;
}
.arrow-right{
right: 1rem;
}
.circle-indicators{
display: flex;
position: absolute;
bottom: 1rem;
}
.current-indicator{
background-color:#ffffff;
height: 15px;
width: 15px;
border-radius: 50%;
border: none;
outline: none;
margin: 0 0.2rem;
cursor: pointer;
}
.inactive-indicator {
background-color: gray;
}

Imports and file setup

import { useEffect, useState } from 'react'
import { BsArrowLeftCircleFill, BsArrowRightCircleFill } from 'react-icons/bs'
import './styles.css'
  • useState and useEffect are React hooks:
  • useState stores and updates local component state.
  • useEffect runs side effects like fetching data.
  • react-icons/bs provides Bootstrap icons for arrows; they are simple SVG components you can click.
  • styles.css contains all styling. Keeping CSS separate is fine for small components.

Component signature and props

export default function ImageSlider ({ url, limit = 5, page = 1 }) {
  • url (required): base API endpoint to fetch images from. Example: “https://example.com/photos”.
  • limit: how many images to request per fetch (default 5).
  • page: page number to request from the API (default 1).
  • Default props are provided inline via ES6 default parameters.

State variables

const [images, setImages] = useState([])
const [currentSlide, setCurrentSlide] = useState(0)
const [errorMsg, setErrorMsg] = useState(null)
const [loading, setLoading] = useState(false)
  • images: array of image objects returned by the API.
  • currentSlide: index of the currently visible image (0-based).
  • errorMsg: if fetch fails, store the error message and show it.
  • loading: boolean to show a loading indicator while the fetch runs.

Fetch function

async function fetchImages (getUrl) {
try {
setLoading(true)
const response = await fetch(`${getUrl}?page=${page}&limit=${limit}`)
const data = await response.json()
if (data) {
setImages(data)
setLoading(false)
}
} catch (e) {
setErrorMsg(e.message)
setLoading(false)
}
}
  • fetchImages builds the full URL adding ?page=…&limit=….
  • fetch returns a Response — calling .json() converts it to a JavaScript object/array.
  • On success: store data in images and clear loading.
  • On error: set errorMsg to e.message, so the UI shows a readable message, and clear loading.
  • Note: this code assumes the API returns JSON array of image objects. If the API returns a different shape, adapt accordingly.

Navigation handlers

function handlePrevious () {
setCurrentSlide(currentSlide === 0 ? images.length - 1 : currentSlide - 1)
}

function handleNext () {
setCurrentSlide(currentSlide === images.length - 1 ? 0 : currentSlide + 1)
}

handlePrevious

  • If we’re on the first slide (index 0), wrap to the last slide (images.length – 1).
  • Otherwise move to currentSlide – 1.

handleNext

  • If we’re on the last slide, wrap to the first (index 0).
  • Otherwise move to currentSlide + 1.

This creates a circular slider (no hard stops).

useEffect — calling fetch on mount or URL change

useEffect(() => {
if (url !== '') fetchImages(url)
}, [url])
  • Runs once when the component mounts and whenever url changes.
  • Guards with if (url !== ”) so it does not attempt to fetch empty string URL.
  • Note: page and limit are not in dependency array. If you want fetch to re-run when page or limit change, add them to the array: [url, page, limit].

Simple loading and error UI

if (loading) {
return <div>Loading data ! Please wait</div>
}

if (errorMsg !== null) {
return <div>Error occured ! {errorMsg}</div>
}
  • Early returns simplify the main render. When loading, the component shows only the loading message. When an error occurs, it shows the error.
  • This is a straightforward pattern to avoid rendering the slider when there is no data.

Rendering the slider

return (
<div className='container'>
<BsArrowLeftCircleFill
onClick={handlePrevious}
className='arrow arrow-left'
/>
{images && images.length
? images.map((imageItem, index) => (
<img
key={imageItem.id}
alt={imageItem.download_url}
src={imageItem.download_url}
className={
currentSlide === index
? 'current-image'
: 'current-image hide-current-image'
}
/>
))
: null}
<BsArrowRightCircleFill
onClick={handleNext}
className='arrow arrow-right'
/>
<span className='circle-indicators'>
{images && images.length
? images.map((_, index) => (
<button
key={index}
className=
{currentSlide === index
? 'current-indicator'
: 'current-indicator inactive-indicator'}
onClick={() => setCurrentSlide(index)}
></button>
))
: null}
</span>
</div>
)
  • Arrow icons are placed inside container and styled absolutely with CSS so they float over the images.
  • images.map(…) creates an <img> for each item:
  • key={imageItem.id}: unique key for React rendering. Ensure your API returns unique ids.
  • src={imageItem.download_url}: the actual image URL.
  • alt={imageItem.download_url}: better to replace with a descriptive alt text if available (e.g., imageItem.author or imageItem.title).
  • The class toggles between ‘current-image’ (visible) and ‘current-image hide-current-image’ (hidden) based on currentSlide === index.
  • Indicators:
  • Rendered as <button> elements for accessibility (keyboard focus possible).
  • Clicking a dot onClick={() => setCurrentSlide(index)} jumps to that slide.

CSS explanation (what each block does)

.container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 600px;
height: 450px;
}
  • position: relative: anchors absolutely-positioned children (arrows, indicators).
  • Fixed width/height: slider dimension. For real apps, make responsive with % or max-width.
.current-image{
border-radius: 0.5rem;
box-shadow: 0px 0px 7px #666;
width: 100%;
height: 100%;
}
.hide-current-image{
display: none;
}
  • current-image covers container and has rounded corners and shadow.
  • hide-current-image hides non-active images via display: none — simple and effective.
.arrow{
position: absolute;
width: 2rem;
height: 2rem;
color: #fff;
filter: drop-shadow(0px 0px 5px #555);
}
.arrow-left{
left:1rem;
}
.arrow-right{
right: 1rem;
}
  • Arrow icons are absolutely positioned and use drop shadow for visibility.
.circle-indicators{
display: flex;
position: absolute;
bottom: 1rem;
}
.current-indicator{
background-color:#ffffff;
height: 15px;
width: 15px;
border-radius: 50%;
border: none;
outline: none;
margin: 0 0.2rem;
cursor: pointer;
}
.inactive-indicator {
background-color: gray;
}
  • Indicators are centered horizontally because the .container uses flex centering and indicators are absolute bottom.
  • Active indicator white, inactive gray.

Important details, gotchas, and assumptions

API shape

Your code assumes the API returns an array of objects where each object has id and download_url properties. If the API returns a wrapper { data: […] }, adapt setImages(data.data) or similar.

Undefined images.length

When images are empty, images.length is zero. Handled by conditional rendering. But if images is not an array, map will fail. Ensure data is always an array.

Key for list items

Using imageItem.id is good. If IDs are not unique, React rendering may misbehave.

Accessibility

Indicators are <button> elements, which is good. Consider adding aria-label to arrows and buttons to improve screen reader support:

<BsArrowLeftCircleFill aria-label="previous image" ... /> <button aria-label={`Go to slide ${index + 1}`} ... />

Keyboard control

Add keyboard handlers (left/right arrow keys) to move slides for accessibility:

useEffect(() => {
function onKey(e) {
if (e.key === 'ArrowLeft') handlePrevious();
if (e.key === 'ArrowRight') handleNext();
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey); }
, [currentSlide, images]);

Responsive design

The CSS uses fixed width/height. Use %, max-width, or media queries to make it work on small screens.

Auto-play / timer

To auto-advance slides, use setInterval inside useEffect, and clear interval on unmount.

Image loading UX

Consider lazy-loading images or showing a low-res placeholder until the image loads to avoid flicker.

Handling empty or bad url

You check if (url !== ”) before fetching. Also handle null or undefined gracefully.

CORS and JSON errors

If response.json() throws because the server returned HTML (unexpected token <), check the API URL, CORS headers, and whether the API requires authentication.

Dependencies inside useEffect

  • Currently useEffect depends only on [url]. If page or limit can change after mount, include them: [url, page, limit].

Examples of small improvements (code snippets)

Add aria labels to arrows and buttons

<BsArrowLeftCircleFill
onClick={handlePrevious}
className='arrow arrow-left'
role="button"
aria-label="Previous slide"
/>

Make fetch re-run on page/limit change

useEffect(() => {
if (url) fetchImages(url)
}, [url, page, limit])

Guard against empty API result shape

const data = await response.json()
const imagesArray = Array.isArray(data) ? data : (data.results || data.data || [])
setImages(imagesArray)

Add keyboard controls
(see previous section for full snippet)

How to use the component

Place the component in your app and pass the image API URL:

import ImageSlider from './ImageSlider'

function App() {
return (
<div>
<ImageSlider url="https://picsum.photos/v2/list" limit={5} page={1} />
</div>
)
}
  • https://picsum.photos/v2/list is an example endpoint that returns an array of images with id and download_url.

Conclusion

  • The component fetches images from url?page=…&limit=…, stores them in state, and renders one visible image at a time.
  • Navigation works by updating currentSlide and wrapping around at the ends.
  • Indicators let the user jump to any slide.
  • CSS hides non-active images and positions arrows and indicators.
  • The code is clear and easy to extend — add keyboard controls, responsiveness, lazy loading, or automatic play as next steps.

Visit the repo

Happy coding!


Building a Complete React Image Slider Component was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.

This post first appeared on Read More