前后对比滑块
媒体 · 区块 · 已复制 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 / 步长 5 | 50 |
分割线颜色split | 颜色 | #111827 |
显示角标labels | 开关 | 开 |
上方的调参预览可以直接拖动,改完点「复制当前代码」拿走带你自己参数的版本。
相似组件
按标签重合度推荐,不是运营手配的榜单。



