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 divs:

  • 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 divs 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 is position: 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 with left: 0; right: 0; and transform: translateY(-50%);
  • The .range-slider__thumb is also centered but with transform: 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 and cursor: 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 the position state
  • onMouseDown and onTouchStart 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:

  1. We get the bounding rectangle of the slider track element
  2. We calculate the click/touch position as a percentage of the track width (0-100)
  3. We clamp the position between 0 and 100 with Math.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 and position state are initialized to initialValue
  • A new effect syncs the initial position to match the value 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 and max 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *