How to Build a Range Slider Component in React from Scratch Using Only Divs and Spans
Range sliders are versatile UI controls that let users select a value or range of values by dragging a thumb along a track. While there are plenty of pre-built slider components available for React, building your own from scratch is a great way to understand how they work under the hood. In this post, we‘ll walk through creating a custom range slider in React using only simple <div>
and <span>
elements.
Here‘s what the final slider component will look like:
[Example image of range slider]Setting Up the Basic Structure
Let‘s start by defining the skeleton of our RangeSlider
component:
const RangeSlider = () => {
return (
<div className="range-slider">
<div className="range-slider__track"></div>
<div className="range-slider__thumb"></div>
</div>
);
};
Here we have a parent div
that will contain the slider elements. Inside are two child div
s:
- The
range-slider__track
will be the clickable/touchable area the thumb moves along - The
range-slider__thumb
will be the draggable handle the user controls
We‘ll use CSS to style these to look like a slider.
Styling the Slider Elements
Let‘s add some styles to turn our div
s into a functional slider:
.range-slider {
position: relative;
width: 200px;
height: 20px;
}
.range-slider__track {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
height: 5px;
background-color: #ddd;
border-radius: 5px;
}
.range-slider__thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
background-color: #007bff;
border-radius: 50%;
cursor: pointer;
}
The key aspects:
- The parent
.range-slider
isposition: relative
so child elements can be positioned inside it - The
.range-slider__track
spans the full width/height of the slider and is horizontally centered withleft: 0; right: 0;
andtransform: translateY(-50%);
- The
.range-slider__thumb
is also centered but withtransform: translate(-50%, -50%);
since we want its center to align with the ends of the track - The thumb gets a
border-radius: 50%;
to make it round andcursor: pointer;
for visual feedback
Now our elements are styled to resemble a slider!
Making the Slider Interactive
Next, let‘s make the slider thumb draggable. We‘ll use React‘s useState
hook to store the thumb position in the component‘s state:
import React, { useState } from ‘react‘;
const RangeSlider = () => {
const [position, setPosition] = useState(0);
return (
<div className="range-slider">
...
<div
className="range-slider__thumb"
style={{ left: `${position}%` }}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
></div>
</div>
);
};
We‘ve added:
- A
position
state variable (default 0) that will represent the thumb position from 0 to 100 - An inline
left
style on the thumb div so it‘s positioned according to theposition
state onMouseDown
andonTouchStart
handlers so we can register when a drag interaction starts
Now let‘s implement the handleMouseDown
and handleTouchStart
functions:
const handleMouseDown = (event) => {
document.addEventListener(‘mousemove‘, handleMouseMove);
document.addEventListener(‘mouseup‘, handleMouseUp);
};
const handleTouchStart = (event) => {
document.addEventListener(‘touchmove‘, handleTouchMove);
document.addEventListener(‘touchend‘, handleTouchEnd);
};
When a mousedown
or touchstart
event occurs on the thumb, we attach mousemove
/touchmove
and mouseup
/touchend
listeners to the document. This allows us to track the thumb even if the cursor/finger moves outside it.
The handleMouseMove
/handleTouchMove
functions will be responsible for updating the position
state as the user drags:
const handleMouseMove = (event) => {
const track = event.target.parentNode.getBoundingClientRect();
const position = (event.clientX - track.left) / track.width * 100;
setPosition(Math.min(Math.max(position, 0), 100));
};
const handleTouchMove = (event) => {
const track = event.target.parentNode.getBoundingClientRect();
const position =
(event.touches[0].clientX - track.left) / track.width * 100;
setPosition(Math.min(Math.max(position, 0), 100));
};
Here‘s what‘s happening:
- We get the bounding rectangle of the slider track element
- We calculate the click/touch
position
as a percentage of the track width (0-100) - We clamp the
position
between 0 and 100 withMath.min/max
and update the state
When the user releases the thumb, we remove the event listeners:
const handleMouseUp = () => {
document.removeEventListener(‘mousemove‘, handleMouseMove);
document.removeEventListener(‘mouseup‘, handleMouseUp);
};
const handleTouchEnd = () => {
document.removeEventListener(‘touchmove‘, handleTouchMove);
document.removeEventListener(‘touchend‘, handleTouchEnd);
};
At this point, we have a functional range slider! The user can drag the thumb to change its position. However, clicking the track doesn‘t do anything yet.
Adding Click-to-Reposition
It would be nice if users could click anywhere on the track to reposition the thumb there. Let‘s add that feature:
<div
className="range-slider__track"
onClick={handleTrackClick}
></div>
// ...
const handleTrackClick = (event) => {
const track = event.target.getBoundingClientRect();
const position = (event.clientX - track.left) / track.width * 100;
setPosition(position);
};
We attach an onClick
handler to the track div. When fired, it calculates the click position using the same logic as the thumb dragging and updates the position
state directly. Now clicking the track will move the thumb!
Displaying the Selected Value
To make the slider more informative, let‘s display the currently selected value. First we‘ll add a new piece of state to store the value:
const [value, setValue] = useState(0);
And render it in the component:
<div className="range-slider__value">{value}</div>
Now we need to update the value
whenever the position
changes. We can do this with an useEffect
hook:
useEffect(() => {
setValue(Math.round(position));
}, [position]);
Whenever position
changes, this effect will run and update value
to match (rounded to the nearest integer).
Let‘s also allow the slider to be initialized with a default value
prop:
const RangeSlider = ({ initialValue = 0 }) => {
const [value, setValue] = useState(initialValue);
const [position, setPosition] = useState(initialValue);
useEffect(() => {
setPosition(value);
}, []);
// ...
};
We‘ve changed:
- The component accepts an optional
initialValue
prop (default 0) - Both
value
andposition
state are initialized toinitialValue
- A new effect syncs the initial
position
to match thevalue
on mount
Customizing the Slider
Our range slider is looking good, but what if we want to customize its behavior more? Here are a few additional props we can add:
const RangeSlider = ({
initialValue = 0,
min = 0,
max = 100,
step = 1,
}) => {
// ...
useEffect(() => {
const newValue = Math.round((position / 100) * (max - min) / step) * step + min;
setValue(newValue);
}, [position]);
// ...
};
We‘ve introduced:
min
andmax
props to define the slider range- A
step
prop to control the increments the value can change by - Updated the effect to calculate
value
based on these new settings.
Now the parent component can configure the slider as needed:
<RangeSlider initialValue={25} min={0} max={50} step={5} />
Putting It All Together
Here‘s the complete code for our range slider component:
import React, { useState, useEffect } from ‘react‘;
const RangeSlider = ({
initialValue = 0,
min = 0,
max = 100,
step = 1,
}) => {
const [value, setValue] = useState(initialValue);
const [position, setPosition] = useState(initialValue);
useEffect(() => {
setPosition(value);
}, []);
useEffect(() => {
const newValue = Math.round((position / 100) * (max - min) / step) * step + min;
setValue(newValue);
}, [position]);
const handleTrackClick = (event) => {
const track = event.target.getBoundingClientRect();
const position = (event.clientX - track.left) / track.width * 100;
setPosition(position);
};
const handleMouseDown = (event) => {
document.addEventListener(‘mousemove‘, handleMouseMove);
document.addEventListener(‘mouseup‘, handleMouseUp);
};
const handleTouchStart = (event) => {
document.addEventListener(‘touchmove‘, handleTouchMove);
document.addEventListener(‘touchend‘, handleTouchEnd);
};
const handleMouseUp = () => {
document.removeEventListener(‘mousemove‘, handleMouseMove);
document.removeEventListener(‘mouseup‘, handleMouseUp);
};
const handleTouchEnd = () => {
document.removeEventListener(‘touchmove‘, handleTouchMove);
document.removeEventListener(‘touchend‘, handleTouchEnd);
};
const handleMouseMove = (event) => {
const track = event.target.parentNode.getBoundingClientRect();
const position = (event.clientX - track.left) / track.width * 100;
setPosition(Math.min(Math.max(position, 0), 100));
};
const handleTouchMove = (event) => {
const track = event.target.parentNode.getBoundingClientRect();
const position =
(event.touches[0].clientX - track.left) / track.width * 100;
setPosition(Math.min(Math.max(position, 0), 100));
};
return (
<div className="range-slider">
<div
className="range-slider__track"
onClick={handleTrackClick}
></div>
<div
className="range-slider__thumb"
style={{ left: `${position}%` }}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
></div>
<div className="range-slider__value">{value}</div>
</div>
);
};
export default RangeSlider;
Conclusion
And there you have it! We‘ve built a functional, customizable range slider component in React using only simple div
and span
elements.
Some key concepts we covered:
- Using
useState
to store and update the slider‘s state - Applying styles conditionally to move the slider thumb
- Calculating relative click/touch positions to update the slider value
- Extracting behavior into props to make the component flexible
Hopefully this gives you a solid foundation for understanding how range sliders work and creating your own variations. Try expanding on this implementation with additional features like multiple thumbs, configurable styling props, and more.