UI Bits

前后对比滑块

媒体 · 区块 · 已复制 0 次

React

左右拖动分割线看改造前后。两层都是真 DOM,不是两张图拼的——所以右边那层照样能交互、能读屏。分割用 clip-path 而不是宽度,拖动时不触发重排;指针用 setPointerCapture 接管,手滑出容器也不会中断。滑块本身是 role="slider",方向键微调、Home/End 到两端,键盘用户不需要鼠标。

调参预览

订单列表共 1,284 条
订单客户金额状态渠道时间
#1000—¥39———
#1011—¥78———
#1022—¥117———
#1033—¥156———
#1044—¥195———
#1055—¥234———
#1066—¥273———
改造前改造后
50
#111827
#111827
开

源码

'use client'

import { useRef, useState } from 'react'

/**
 * 前后对比。before / after 都是 ReactNode —— 不给图片 URL,
 * 因为真 DOM 才能保住文字可选中、链接可点、读屏可读。
 */
export function CompareSlider({
  before,
  after,
  position = 50,
  split = '#ffffff',
  labels = true,
}: {
  before: React.ReactNode
  after: React.ReactNode
  position?: number
  split?: string
  labels?: boolean
}) {
  const [pos, setPos] = useState(position)
  const boxRef = useRef<HTMLDivElement>(null)

  function setFromClientX(clientX: number) {
    const el = boxRef.current
    if (!el) return
    const rect = el.getBoundingClientRect()
    const next = ((clientX - rect.left) / rect.width) * 100
    setPos(Math.min(100, Math.max(0, next)))
  }

  return (
    <div ref={boxRef} className="relative overflow-hidden rounded-xl border border-gray-200">
      {/* 下层:改造前,铺满 */}
      <div className="relative">{before}</div>

      {/* 上层:改造后,从左边裁掉 pos%,露出下面那层 */}
      <div
        className="absolute inset-0"
        style={{ clipPath: `inset(0 0 0 ${pos}%)` }}
        aria-hidden
      >
        {after}
      </div>

      {labels && (
        <>
          <span className="absolute left-2 top-2 rounded bg-black/50 px-1.5 py-0.5 text-[10px] text-white">
            改造前
          </span>
          <span className="absolute right-2 top-2 rounded bg-black/50 px-1.5 py-0.5 text-[10px] text-white">
            改造后
          </span>
        </>
      )}

      {/* 手柄:既是指针目标也是键盘目标 */}
      <div
        role="slider"
        tabIndex={0}
        aria-label="对比分割位置"
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={Math.round(pos)}
        onPointerDown={(e) => {
          // 接管指针:手滑出容器也不会丢事件
          e.currentTarget.setPointerCapture(e.pointerId)
          setFromClientX(e.clientX)
        }}
        onPointerMove={(e) => {
          if (e.currentTarget.hasPointerCapture(e.pointerId)) setFromClientX(e.clientX)
        }}
        onKeyDown={(e) => {
          const step = e.shiftKey ? 10 : 2
          if (e.key === 'ArrowLeft') setPos((p) => Math.max(0, p - step))
          else if (e.key === 'ArrowRight') setPos((p) => Math.min(100, p + step))
          else if (e.key === 'Home') setPos(0)
          else if (e.key === 'End') setPos(100)
        }}
        className="absolute inset-y-0 w-11 -translate-x-1/2 cursor-ew-resize
                   touch-none outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
        style={{ left: `${pos}%` }}
      >
        <span
          aria-hidden
          className="absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2"
          style={{ background: split }}
        />
        <span
          aria-hidden
          className="absolute left-1/2 top-1/2 flex h-7 w-7 -translate-x-1/2 -translate-y-1/2
                     items-center justify-center rounded-full bg-white text-gray-500 shadow"
        >
          ‹›
        </span>
      </div>
    </div>
  )
}

可调参数

参数类型默认值
初始位置position数值 0–100 / 步长 550
分割线颜色split颜色#111827
显示角标labels开关开

上方的调参预览可以直接拖动,改完点「复制当前代码」拿走带你自己参数的版本。

相似组件

按标签重合度推荐,不是运营手配的榜单。

命令面板 预览

命令面板

导航 · 组件

定价卡片 预览

定价卡片

数据展示 · 组件

光标光斑卡片 预览

光标光斑卡片

布局 · 元素

流光按钮 预览

流光按钮

其他 · 元素