Building an Advanced Slider Component

In this post, I want to share how I built a custom slider component that comes packed with useful features. This slider not only supports basic functionalities like step increments and configurable maximum/minimum values, but it also offers advanced features such as displaying scale tick values, suffix rendering, and custom value formatting. Whether you’re building a finance dashboard, a settings panel, or a data visualization tool, this component can be tailored to your needs.

Features

Step Increments

The slider supports step increments, allowing you to specify how much the slider’s value should increase or decrease with each move. This is particularly useful for ensuring users select values within a desired precision.

Scale Tick Values

To improve usability, the slider renders tick values along its track. These ticks provide a visual reference for users, making it easier to gauge the range and current value.

Maximum and Minimum Values

You can easily define the slider’s range by setting the maximum and minimum values. This ensures that users can only select a value within a valid, predetermined range.

Suffix Rendering

Often, slider values need to be accompanied by a unit (e.g., %, $, kg). With suffix rendering, you can easily append a suffix to the displayed value, making it clear what the number represents.

Custom Value Formatting

In some cases, the raw slider value might not be in the ideal format for display. This slider component allows you to pass a formatter function that converts the numeric value into a user-friendly string format.

Implementation Overview

Below is a simplified example of how the slider component is implemented using React and TypeScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
* @author Edward <wang.huiyang@outlook.com>
* @created 2025-02-08
*/

import { CustomColorLight } from "@/styles/customColor"
import React, { useEffect, useRef, useState } from "react"

const USlider: React.FC<{
suffix?: string
min?: number
max?: number
step?: number
markStep?: number
initialValue?: number
customPoints?: number[]
onChange?: (value: number) => void
numFormat?: (value: number) => string
}> = ({
suffix,
min = 0,
max = 100,
step = 1,
markStep = 5,
onChange,
initialValue = 50,
customPoints,
numFormat,
}) => {
const [value, setValue] = useState<number>(initialValue) // slider value
const [isDragging, setIsDragging] = useState<boolean>(false)
const sliderRef = useRef<HTMLDivElement>(null)

// calculate percentage of current value
const getPercentage = (val: number) => ((val - min) / (max - min)) * 100

// handle slider move event
const handleMove = (clientX: number) => {
if (!sliderRef.current) return

const sliderRect = sliderRef.current.getBoundingClientRect()
const offsetX = clientX - sliderRect.left
const sliderWidth = sliderRect.width

// calculate current value
let newValue = Math.round((offsetX / sliderWidth) * (max - min) + min)
newValue = Math.max(min, Math.min(max, newValue))
newValue = Math.round(newValue / step) * step

setValue(newValue)
onChange && onChange(newValue)
window.Telegram.WebApp.HapticFeedback.selectionChanged()
}

// handle slider click event
const handleMouseDown = () => setIsDragging(true)
const handleMouseUp = () => setIsDragging(false)
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging) handleMove(e.clientX)
}
const handleTouchMove = (e: React.TouchEvent) => {
if (isDragging) handleMove(e.touches[0].clientX)
}

// generate ticks
const generateTicks = () => {
const ticks = []
for (let i = min; i <= max; i += markStep) {
ticks.push(
<div
key={i}
className="absolute transform -translate-x-1/2 u-font-12 text-[#666]"
style={{ left: `${getPercentage(i)}%` }}
>
{numFormat ? numFormat(i) : i}
{suffix}
</div>
)
}
return ticks
}

const generateCustomTicks = () => {
if (!customPoints) {
return
}

const ticks = []
for (let i = 0; i < customPoints.length; i++) {
const val = customPoints[i]

if (val >= min && val <= max) {
ticks.push(
<div
key={val}
className="absolute transform -translate-x-1/2 u-font-12 text-[#666]"
style={{ left: `${getPercentage(val)}%` }}
>
{numFormat ? numFormat(val) : val}
{suffix}
</div>
)
}
}
return ticks
}

const generateTickPoints = () => {
const ticks = []
for (let i = min; i <= max; i += markStep) {
ticks.push(
<div
key={i}
className="text-xs text-gray-500 absolute transform -top-[3px] -translate-x-1/2 w-2 h-2 rounded-full border-2 bg-bg-secondary dark:bg-bg-secondary-dark"
style={{
left: `${getPercentage(i)}%`,
borderColor: CustomColorLight.cvip,
}}
onClick={() => {
setValue(i)
onChange && onChange(i)
window.Telegram.WebApp.HapticFeedback.selectionChanged()
}}
></div>
)
}
return ticks
}

const generateCustomPoints = () => {
if (!customPoints) {
return
}

const ticks = []

for (let i = 0; i < customPoints.length; i++) {
const val = customPoints[i]

if (val >= min && val <= max) {
ticks.push(
<div
key={val}
className="text-xs text-gray-500 absolute transform -top-[3px] -translate-x-1/2 w-2 h-2 rounded-full border-2 bg-bg-secondary dark:bg-bg-secondary-dark"
style={{
left: `${getPercentage(val)}%`,
borderColor: CustomColorLight.cvip,
}}
onClick={() => {
setValue(val)
onChange && onChange(val)
window.Telegram.WebApp.HapticFeedback.selectionChanged()
}}
></div>
)
}
}

return ticks
}

useEffect(() => {
setValue(initialValue)
}, [initialValue])

return (
<div className="relative mt-10 overflow-visible mx-[12px]">
{/* slider track */}
<div
ref={sliderRef}
className="w-full h-[2px] bg-[#eee] rounded-full relative cursor-pointer overflow-visible"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchStart={handleMouseDown}
onTouchMove={handleTouchMove}
onTouchEnd={handleMouseUp}
>
{/* slider progress */}
<div
className="h-[2px] bg-cvip rounded-full absolute"
style={{ width: `${getPercentage(value)}%` }}
></div>

{/* slider handle */}
<div
className="w-[16px] h-[16px] top-[1px] rounded-full absolute -translate-x-1/2 -translate-y-1/2 border-2 bg-bg-secondary dark:bg-bg-secondary-dark z-50"
style={{
left: `${getPercentage(value)}%`,
borderColor: CustomColorLight.cvip,
}}
></div>

{/* tick points */}
<div>
{customPoints && customPoints.length > 0
? generateCustomPoints()
: generateTickPoints()}
</div>
</div>

{/* ticks */}
<div className="relative h-[20px] mt-[8px] flex justify-between items-center overflow-visible">
{customPoints && customPoints.length > 0
? generateCustomTicks()
: generateTicks()}
</div>

{/* floating display current value */}
<div
className="absolute -top-8 px-[4px] py-[2px] bg-cvip rounded u-font-12 text-black font-bold"
style={{
left: `${getPercentage(value)}%`,
transform: "translateX(-50%)",
display: isDragging ? "block" : "none",
}}
>
{numFormat ? numFormat(value) : value}
{suffix}
</div>
</div>
)
}

export default USlider

Usage Example

Below is an example of how to integrate the Slider component into your application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div className="-mx-[12px] px-[12px]">
<USlider
min={min}
max={max}
initialValue={leverage}
onChange={handleSliderChange}
customPoints={[1, 3, 5, 10, 15]}
suffix="x"
/>

<USlider
min={0}
max={200}
initialValue={100}
customPoints={[0, 50, 100, 150, 200]}
numFormat={(value) => ((value - 100) / 100).toFixed(2)}
/>
</div>

Demo

Conclusion

The custom slider component described in this post is versatile and can be easily integrated into any React application. With features like step increments, tick values, customizable ranges, suffix rendering, and value formatting, it’s a powerful tool for creating interactive and user-friendly interfaces.

Feel free to customize and extend the component further to meet your project’s specific requirements. Happy coding!