Skip to main content

React Dual Range Price Slider with Min and Max Input

· 6 min read
Kamlesh
Quality Assurance @TestKarts

Introduction

Price range filters are crucial for e-commerce platforms, allowing users to narrow down products based on their budget. Our goal is to build a React component that provides a user-friendly interface for selecting a price range, with synchronized slider and input values, and optimized performance.

The Plan

The CatlogPriceFilter component will have the following key features:

  1. Range Slider: Users can adjust minimum and maximum values using a range slider.
  2. Input Fields: Numeric input fields allow users to manually input the price range.
  3. Synchronized State: The slider and input fields will be in sync, meaning changes in one will reflect in the other.
  4. Price Change Event: A callback function will be triggered when the user finishes adjusting the price range.

Table of Contents

Let's dive into the code!

Component Setup

We start by defining our React component and setting up the initial state. This includes initial min and max price values and state variables for managing the slider and input fields.alt text

import React, { useState, useEffect } from "react";
import "./catlogPriceFilter.css";

const CatlogPriceFilter = () => {
const initialMinPrice = 0;
const initialMaxPrice = 1000;

const [sliderMinValue] = useState(initialMinPrice);
const [sliderMaxValue] = useState(initialMaxPrice);

const [minVal, setMinVal] = useState(initialMinPrice);
const [maxVal, setMaxVal] = useState(initialMaxPrice);
const [minInput, setMinInput] = useState(initialMinPrice);
const [maxInput, setMaxInput] = useState(initialMaxPrice);

const [isDragging, setIsDragging] = useState(false);

const minGap = 5;

Handling Slider and Input Changes

We need to handle changes from both the slider and the input fields. This involves updating the component state based on user input and ensuring that the values remain synchronized.

  const slideMin = (e) => {
const value = parseInt(e.target.value, 10);
if (value >= sliderMinValue && maxVal - value >= minGap) {
setMinVal(value);
setMinInput(value);
}
};

const slideMax = (e) => {
const value = parseInt(e.target.value, 10);
if (value <= sliderMaxValue && value - minVal >= minGap) {
setMaxVal(value);
setMaxInput(value);
}
};

Updating the Slider Track

To provide visual feedback, we need to dynamically update the slider track to reflect the selected price range. This involves calculating the percentage of the slider's width that corresponds to the selected range.

  const setSliderTrack = () => {
const range = document.querySelector(".slider-track");

if (range) {
const minPercent =
((minVal - sliderMinValue) / (sliderMaxValue - sliderMinValue)) * 100;
const maxPercent =
((maxVal - sliderMinValue) / (sliderMaxValue - sliderMinValue)) * 100;

range.style.left = `${minPercent}%`;
range.style.right = `${100 - maxPercent}%`;
}
};

useEffect(() => {
setSliderTrack();
}, [minVal, maxVal]);

Synchronizing Inputs with Slider

To ensure that input fields reflect the current slider values and vice versa, we handle input changes and keyboard events. This synchronization keeps the UI consistent and responsive.

  const handleMinInput = (e) => {
const value =
e.target.value === "" ? sliderMinValue : parseInt(e.target.value, 10);
if (value >= sliderMinValue && value < maxVal - minGap) {
setMinInput(value);
setMinVal(value);
}
};

const handleMaxInput = (e) => {
const value =
e.target.value === "" ? sliderMaxValue : parseInt(e.target.value, 10);
if (value <= sliderMaxValue && value > minVal + minGap) {
setMaxInput(value);
setMaxVal(value);
}
};

const handleInputKeyDown = (e, type) => {
if (e.key === "Enter") {
const value = parseInt(e.target.value, 10);
if (
type === "min" &&
value >= sliderMinValue &&
value < maxVal - minGap
) {
setMinVal(value);
} else if (
type === "max" &&
value <= sliderMaxValue &&
value > minVal + minGap
) {
setMaxVal(value);
}
}
};

Handling User Interactions

To enhance user experience, we handle drag events for the slider to provide real-time feedback and ensure smooth interaction.

  const startDrag = () => {
setIsDragging(true);
};

const stopDrag = () => {
setIsDragging(false);
};

Complete JSX Structure

Here's the complete JSX structure for our component, which includes the range slider, input fields, and tooltips for displaying the current values.

  return (
<div className="double-slider-box">
<div className="input-box">
<div className="min-box">
<input
type="number"
value={minInput}
onChange={handleMinInput}
onKeyDown={(e) => handleInputKeyDown(e, "min")}
className="min-input"
min={sliderMinValue}
max={maxVal - minGap}
/>
</div>
<div className="max-box">
<input
type="number"
value={maxInput}
onChange={handleMaxInput}
onKeyDown={(e) => handleInputKeyDown(e, "max")}
className="max-input"
min={minVal + minGap}
max={sliderMaxValue}
/>
</div>
</div>
<div className="range-slider">
<div className="slider-track"></div>
<input
type="range"
min={sliderMinValue}
max={sliderMaxValue}
value={minVal}
onChange={slideMin}
onMouseDown={startDrag}
onMouseUp={stopDrag}
onTouchStart={startDrag}
onTouchEnd={stopDrag}
className="min-val"
/>
<input
type="range"
min={sliderMinValue}
max={sliderMaxValue}
value={maxVal}
onChange={slideMax}
onMouseDown={startDrag}
onMouseUp={stopDrag}
onTouchStart={startDrag}
onTouchEnd={stopDrag}
className="max-val"
/>
{isDragging && <div className="min-tooltip">{minVal}</div>}
{isDragging && <div className="max-tooltip">{maxVal}</div>}
</div>
</div>
);
};

export default CatlogPriceFilter;

Final CSS

.double-slider-box {
background-color: #fff;
border-radius: 10px;
padding: 20px;
width: 100%;
max-width: 300px;
margin: auto;
}

.range-slider {
position: relative;
width: 100%;
height: 5px;
margin: 30px 0;
background-color: #8a8a8a;
border-radius: 5px;
}

.slider-track {
height: 100%;
position: absolute;
background-color: #fe696a;
left: 0;
right: 100%;
border-radius: 5px;
}

.range-slider input[type="range"] {
position: absolute;
width: 100%;
top: 0;
transform: translateY(-50%);
background: none;
pointer-events: none;
appearance: none;
height: 5px;
}

input[type="range"]::-webkit-slider-thumb {
height: 25px;
width: 25px;
border-radius: 50%;
border: 3px solid #fff;
background: #fe696a;
pointer-events: auto;
appearance: none;
cursor: pointer;
box-shadow: 0 0.125rem 0.5625rem -0.125rem rgba(0, 0, 0, 0.25);
position: relative;
z-index: 2; /* Ensure thumbs appear above the track */
}

input[type="range"]::-moz-range-thumb {
height: 25px;
width: 25px;
border-radius: 50%;
border: 3px solid #fff;
background: #fe696a;
pointer-events: auto;
cursor: pointer;
box-shadow: 0 0.125rem 0.5625rem -0.125rem rgba(0, 0, 0, 0.25);
position: relative;
z-index: 2;
}

.input-box {
display: flex;
justify-content: space-between;
width: 100%;
}

.min-box,
.max-box {
width: 50%;
}

.min-box {
margin-right: 10px;
}
.max-box input {
float: right;
}
input[type="number"] {
width: 40px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}

.min-tooltip,
.max-tooltip {
position: absolute;
top: -35px;
font-size: 12px;
color: #555;
background-color: #fff;
padding: 5px;
border: 1px solid #ddd;
border-radius: 5px;
white-space: nowrap;
z-index: 1;
}

.min-tooltip {
left: 0;
transform: translateX(-50%);
}

.max-tooltip {
right: 0;
transform: translateX(50%);
}
/* Chrome, Safari, Edge, and Opera */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}



Conclusion

Creating a dynamic price range filter component in React enhances user interaction by providing intuitive controls for filtering content based on price. By synchronizing input fields with a range slider and optimizing performance, you can offer a seamless user experience. Customize the component to fit your specific needs and integrate it into your React applications to improve user satisfaction.