November 2023
percentage
This slider was designed to create a sleek and engaging way to enable precise user input over a range in an intuitive way. Most users are familiar with typical range inputs that involve dragging a dot or indicator along a line, but this method lacks precision as the available input space is limited to the width of the input.
Part of the magic here was in how the event listeners are set up. Component-scoping the event listeners doesn't suffice in this situation as the range of motion for the user needs to be over the entire document. The four main events that are needed are resize, mousedown/touchstart, mousemove/touchmove, and mouseup/touchend.
The resize event is used to update the screen width variable when the window is resized. The mousedown/touchstart event is used to start the dragging process, and the mousemove/touchmove event is used to update the bar width as the user drags. The mouseup/touchend event is used to stop the dragging process.
When on mobile, you can get the user's touch locations by grabbing the first element in the touch array and accessing the clientX property, by using e.touches[0].clientX.
I used three data attributes to allow for state-based styling without using conditional inline styles for everything (which I find to get very messy, very fast).
data-dragging, data-active, and data-correct are used to style the slider based on various states. For example,
.slider_container[data-dragging="true"][data-active="true"] .bar {
position: absolute;
height: 50px;
width: 100%;
transform: translateX(16px);
background-color: #4d4d4d;
border-radius: 8px 8px 8px 8px;
}
This selector allows me to animate the input bar, to fill the space of it's container, to allow for the precise dragging control on the entire screen. Similarly, I can use the data-correct attribute to style the input bar based on whether or not the user has dragged the input to a specified range or value.
Like any complex enough design, there are some issues I ran into while building this slider.
The main issue I came across was when using the slider to control an actual value stored in a state. The bar width of the slider itself is controlled with the barWidth state, and is updated when the user is dragging relative to the screen width. However, if you need to use the slider to control a value, it's not enough to just call the setBarWidth function and then a setOtherValue that's dependant on the barWidth state.
This is because of the asynchronous nature of state setting in React. The value that depends on the barWidth state will be set before the barWidth state is updated, resulting in a slider that's value is only checked once.
Setting a function scoped newBarWidth variable, then letting that value control both the new value of the barWidth state, and the setOtherValue value ended up being the solution.
Since I've been asked this question a few times now, here's the formula I used to calculate the bar width as the user drags:
const newBarWidth = Math.round(Math.min(Math.max(0, (barWidth + ((e.touches ? e.touches[0].clientX : e.clientX) - initialMouseX) / (screenWidth / sensitivityFactor) * 100)), 100));
It might look a little complicated, but it essentially boils down to this: Set the max value to 100%, and the minimum value to 0%, then calculate the new bar width based on the current mouse/touch position relative to the initial mouse/touch position, divided by the screen width with a sensitivity factor to determine the precision of the drag.