手搓Input range
code
Div版本
js
const Range = ({ value, setValue, maxRange }) => {
const [position, setPosition] = useState(0);
const rangeRef = useRef(null);
const thumbRef = useRef(null);
useEffect(() => {
if (rangeRef.current && thumbRef.current) {
const totalWidth = rangeRef.current.clientWidth - thumbRef.current.clientWidth;
console.log(totalWidth, value, maxRange);
setPosition((value / maxRange) * totalWidth);
}
}, [value, maxRange]);
const handleMove = (e, startX, totalWidth, selfXPosition) => {
const moveX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
let diffX = selfXPosition + moveX - startX;
diffX = Math.max(0, Math.min(diffX, totalWidth));
setPosition(diffX);
const value = ((diffX / totalWidth) * maxRange).toFixed(2);
setValue(value);
};
const handleStart = (e) => {
e.preventDefault();
const totalWidth = rangeRef.current.clientWidth - thumbRef.current.clientWidth;
const startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const selfXPosition = thumbRef.current.offsetLeft;
const moveHandler = (event) => handleMove(event, startX, totalWidth, selfXPosition);
const endHandler = () => {
document.removeEventListener('mousemove', moveHandler);
document.removeEventListener('mouseup', endHandler);
document.removeEventListener('touchmove', moveHandler);
document.removeEventListener('touchend', endHandler);
document.removeEventListener('touchcancel', endHandler);
};
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', endHandler);
document.addEventListener('touchmove', moveHandler);
document.addEventListener('touchend', endHandler);
document.addEventListener('touchcancel', endHandler);
};
const handleClickStart = (e) => {
const rangedom = rangeRef.current
const knobdom = thumbRef.current
const totalWidth = rangedom.clientWidth - knobdom.clientWidth
const left = rangedom.getBoundingClientRect().left
let startX = e.clientX;
let diffx = startX - left
if (diffx <= 0) {
diffx = 0
}
if (diffx >= (rangedom.clientWidth - knobdom.clientWidth)) {
diffx = (rangedom.clientWidth - knobdom.clientWidth)
}
setPosition(diffx)
const v = ((diffx) / totalWidth * maxRange).toFixed(2)
setValue(v);
document.onmousemove = (e) => {
e.stopPropagation()
const endX = e.clientX;
let diffX = startX - left + endX - startX
if (diffX <= 0) {
diffX = 0
}
if (diffX >= (rangedom.clientWidth - knobdom.clientWidth)) {
diffX = (rangedom.clientWidth - knobdom.clientWidth)
}
setPosition(diffX)
const v = (diffX / totalWidth * maxRange).toFixed(2)
setValue(v);
}
document.onmouseup = function () {
e.stopPropagation()
document.onmousemove = null;
document.onmouseup = null;
};
}
return (
<div
className="cursor-pointer w-full h-[11px] rounded-xl p-[3px] bg-[rgba(238,238,238,1)] relative"
ref={rangeRef}
onPointerDown={handleClickStart}
>
<div className="relative w-full h-full">
{[...Array(40 + 1)].map((_, index) => ( //可以模拟step点
<div
style={{
left: `${index * 2.5}%`,
display: `${index % 2 === 0 ? 'none' : 'block'}`
}}
key={index}
className={style.sliderstep}></div>
))}
<div
onMouseDown={handleStart}
onTouchStart={handleStart}
className="absolute-y-center rounded-full w-[12px] h-[12px] bg-[#fff] [box-shadow:0px_0px_4px_0px_rgba(74,0,224,0.4)]"
ref={thumbRef}
style={{ left: `${position}px` }}
/>
<div
className="bg-[#fff] w-full h-full rounded-lg"
style={{
background: `linear-gradient(90deg, rgba(198,178,240,1) ${position}px, #fff ${position}px)`
}}
/>
</div>
</div>
);
};利用原生input range 实现滑块
html
<input className="intensityRange" type="range" min="0" max="1" step="0.01" />
<style>
.intensityRange{
-webkit-appearance: none;
/* background-color: #111; */
outline: none;
overflow: hidden;
border-radius: 15px;
box-shadow: inset 0 0 100px transparent,
/* inset 0 0 10px rgba(255,255,255, 1); */
}
.intensityRange::-webkit-slider-thumb{
-webkit-appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: #fff;
cursor: pointer;
/* 关键点:通过巨大的box-shadow 来展示已经滑过的滑条颜色,要配合上面的overflow */
/* 弊端:滑块按钮会和滑条一样高 */
box-shadow: -407px 0 0 400px rgba(198, 178, 240, 1),
0px 0px 4px 0px rgba(74, 0, 224, 0.4);
}
</style>实现统一样式
两个主要部分
Track 这是滑块的一部分,滑块会沿着它移动。或者换句话说,它是表示可以选择的值范围的长元素。
Thumb 这是轨道上的元素,用户可以移动它来选择不同的范围值。

浏览器的样式实现差异巨大
- Reset (Baseline Styles) 要恢复到Baseline Styles,我们需要四个属性
css
input[type="range"] {
/* 此属性是适用于所有主流浏览器的供应商前缀。通过赋予它此值,none可以告诉每个相应的浏览器清除所有默认样式。 */
-webkit-appearance: none;
appearance: none;
/* 这将清除应用于input的默认背景。 */
background: transparent;
cursor: pointer;
/* 设置input的整体宽度。 */
width: 15rem;
}- Track Styles 我们需要针对不同浏览器特定的供应商前缀,以便在相关元素中应用适当的样式。 任何带有前缀的伪元素-webkit都将应用于 Chrome、Safari、Opera 和 Edge(post-Chromium)浏览器。任何带有前缀的元素都-moz适用于 Firefox。
通常只需要background和height,当然你也可以加上border-radius使得边缘圆润
css
/***** Track Styles *****/
/***** Chrome, Safari, Opera, and Edge Chromium *****/
input[type="range"]::-webkit-slider-runnable-track {
background: #053a5f;
height: 0.5rem;
}
/******** Firefox ********/
input[type="range"]::-moz-range-track {
background: #053a5f;
height: 0.5rem;
}- Thumb Styles (用户移动的中间旋钮)的样式有更多细微差别需要考虑,因为在input range的这一部分,不同浏览器之间存在更多不一致。
- Chrome、Safari 和 Edge Chromium
- 当我们使用
-webkit-appearance: none;清除Thumb的默认样式之后,如果直接设置宽高背景颜色,那么Thumb将不会垂直居中 - 使用以下公式使其垂直居中
上边距 = (Track高度(px)/2) - (Thumb高度(px)/2)
- 当我们使用
css
/***** Thumb Styles *****/
/***** Chrome, Safari, Opera, and Edge Chromium *****/
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
/* (8/2)-(32/2)= 4 - 16 = -12px */
margin-top: -12px; /* Centers thumb on the track */
background-color: #5cd5eb;
height: 2rem;
width: 1rem;
}- FIREFOX
- 他没有不居中的问题,但是他的Thumb会有一个灰色的border,以及border-radius,我们去除即可
css
/***** Thumb Styles *****/
/***** Firefox *****/
input[type="range"]::-moz-range-thumb {
border: none; /*Removes extra border that FF applies*/
border-radius: 0; /*Removes default border-radius that FF applies*/
background-color: #5cd5eb;
height: 2rem;
width: 1rem;
}- Focus Styles
- 删除默认的焦点样式
- 利用为元素重新赋予焦点样式
css
/***** Focus Styles *****/
/* Removes default focus */
input[type="range"]:focus {
outline: none;
}
/***** Chrome, Safari, Opera, and Edge Chromium *****/
input[type="range"]:focus::-webkit-slider-thumb {
border: 1px solid #053a5f;
outline: 3px solid #053a5f;
outline-offset: 0.125rem;
}
/******** Firefox ********/
input[type="range"]:focus::-moz-range-thumb {
border: 1px solid #053a5f;
outline: 3px solid #053a5f;
outline-offset: 0.125rem;
}- 使用JS + CSS变量解决左侧的range范围
使用变量--webkitProgressPercent和背景渐变来决定左右侧的颜色
css
input[type="range"] {
--webkitProgressPercent: 0%;
}
input[type="range"]::-webkit-slider-runnable-track {
background-image: linear-gradient(
90deg,
#f2f2f2 var(--webkitProgressPercent),
#262626 var(--webkitProgressPercent)
);
}使用js控制变量数值
js
const RangeInput = ({
min = 0,
max = 100,
step = 1,
defaultValue = 0,
onChange = () => {}
}) => {
const inputRef = useRef();
const [isChanging, setIsChanging] = useState(false);
const getPercent = useMemo(
() => (value) => ((value - min) / (max - min)) * 100,
[max, min]
);
const changeInputProgressPercentStyle = useCallback(() => {
inputRef.current.style.setProperty(
"--webkitProgressPercent",
`${getPercent(inputRef.current.value)}%`
);
}, [getPercent]);
useEffect(() => {
changeInputProgressPercentStyle();
const inputElement = inputRef.current;
const handleUpAndLeave = () => setIsChanging(false);
const handleDown = () => setIsChanging(true);
// Desktop
inputElement.addEventListener("mousemove", changeInputProgressPercentStyle);
inputElement.addEventListener("mousedown", handleDown);
inputElement.addEventListener("mouseup", handleUpAndLeave);
inputElement.addEventListener("mouseleave", handleUpAndLeave);
// Mobile
inputElement.addEventListener("touchmove", changeInputProgressPercentStyle);
inputElement.addEventListener("touchstart", handleDown);
inputElement.addEventListener("touchend", handleUpAndLeave);
return () => {
inputElement.removeEventListener(
"mousemove",
changeInputProgressPercentStyle
);
inputElement.removeEventListener("mousedown", handleDown);
inputElement.removeEventListener("mouseup", handleUpAndLeave);
inputElement.removeEventListener("mouseleave", handleUpAndLeave);
inputElement.removeEventListener("touchmove", changeInputProgressPercentStyle);
inputElement.removeEventListener("touchstart", handleDown);
inputElement.removeEventListener("touchend", handleUpAndLeave);
};
}, [isChanging, changeInputProgressPercentStyle]);
useEffect(() => {
if (!inputRef?.current) return;
changeInputProgressPercentStyle();
}, [inputRef, changeInputProgressPercentStyle]);
return (
<div>
<input
className="range"
type="range"
ref={inputRef}
min={min}
max={max}
step={step}
value={defaultValue}
onChange={(e) => onChange(e.currentTarget.value)}
/>
</div>
);
};- accent-color 强调色
- 对于左侧的range范围,现代CSS解决方案
accent-color支持使用几行简单的 CSS 为表单元素着色
css
:root {
accent-color: rgba(250, 15, 117);
}