Skip to content

手搓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>

实现统一样式 ​

两个主要部分

  1. Track 这是滑块的一部分,滑块会沿着它移动。或者换句话说,它是表示可以选择的值范围的长元素。

  2. Thumb 这是轨道上的元素,用户可以移动它来选择不同的范围值。

浏览器的样式实现差异巨大

  1. Reset (Baseline Styles) 要恢复到Baseline Styles,我们需要四个属性
css
input[type="range"] {
  /* 此属性是适用于所有主流浏览器的供应商前缀。通过赋予它此值,none可以告诉每个相应的浏览器清除所有默认样式。 */
  -webkit-appearance: none; 
  appearance: none;
  /* 这将清除应用于input的默认背景。 */
  background: transparent; 
  cursor: pointer;
  /* 设置input的整体宽度。 */
  width: 15rem;
}
  1. 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;
}
  1. 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;
}
  1. 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;     
}
  1. 使用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>
  );
};
  1. accent-color 强调色
  • 对于左侧的range范围,现代CSS解决方案accent-color支持使用几行简单的 CSS 为表单元素着色
css
:root {
   accent-color: rgba(250, 15, 117);
}