Skip to content

Instantly share code, notes, and snippets.

@dingyi
Forked from 0xsommer/slide-to-button.tsx
Created November 27, 2024 14:23
Show Gist options
  • Save dingyi/fad2ac9aed09871d2b4c106a39dc7135 to your computer and use it in GitHub Desktop.
Save dingyi/fad2ac9aed09871d2b4c106a39dc7135 to your computer and use it in GitHub Desktop.
Button: Slide to confirm
'use client'
import React, { useEffect, useState } from 'react';
import { motion, useMotionValue, useTransform, animate } from 'framer-motion';
import { IoArrowBackOutline, IoArrowForwardOutline } from "react-icons/io5";
const SlideButton: React.FC = () => {
const [isOn, setIsOn] = useState(false);
const trackRef = React.useRef<HTMLDivElement>(null);
const handleRef = React.useRef<HTMLDivElement>(null);
const [trackWidth, setTrackWidth] = useState(0);
const [handleWidth, setHandleWidth] = useState(0);
useEffect(() => {
if (trackRef.current && handleRef.current) {
const resizeObserver = new ResizeObserver(() => {
setTrackWidth(trackRef.current?.offsetWidth || 0);
setHandleWidth(handleRef.current?.offsetWidth || 0);
});
resizeObserver.observe(trackRef.current);
resizeObserver.observe(handleRef.current);
return () => resizeObserver.disconnect();
}
}, []);
const maxDrag = trackWidth - handleWidth - 8; // 8px for padding
const x = useMotionValue(isOn ? maxDrag : 0);
const background = useTransform(
x,
[0, maxDrag],
['linear-gradient(to right, rgb(156, 163, 175, 0.1), rgb(156, 163, 175, 0.1))', 'linear-gradient(to right, rgb(239, 68, 68), rgb(156, 163, 175, 0.2))']
);
const labelX = useTransform(
x,
[0, maxDrag],
[`-${handleWidth * 0.64}px`, '10px']
);
const rightArrowOpacity = useTransform(
x,
[0, maxDrag / 2, maxDrag],
[1, 0.5, 0]
);
const leftArrowOpacity = useTransform(
x,
[0, maxDrag / 2, maxDrag],
[0, 0.5, 1]
);
const rightArrowX = useTransform(
x,
[0, maxDrag],
[0, 32]
);
const leftArrowX = useTransform(
x,
[0, maxDrag],
[-32, 0]
);
const cancelTextOpacity = useTransform(
x,
[0, maxDrag / 2, maxDrag],
[0, 0.5, 1]
);
const confirmTextOpacity = useTransform(
x,
[0, maxDrag / 2, maxDrag],
[1, 0.5, 0]
);
const handleClick = () => {
const newIsOn = !isOn;
setIsOn(newIsOn);
animate(x, newIsOn ? maxDrag : 0, {
type: "spring",
stiffness: 700,
damping: 30
});
};
const handleDragEnd = () => {
if (x.get() > maxDrag / 2) {
setIsOn(true);
animate(x, maxDrag, {
type: "spring",
stiffness: 700,
damping: 30
});
} else {
setIsOn(false);
animate(x, 0, {
type: "spring",
stiffness: 700,
damping: 30
});
}
};
return (
<div className="w-full flex flex-col items-center gap-8 px-4 py-32">
<motion.div
ref={trackRef}
id="track"
className="relative w-full max-w-[188px] h-12 rounded-full flex items-center shadow-[0px_1.5px_0px_0px_rgba(255,255,255,0.5),inset_0px_4px_4px_0px_rgba(0,0,0,0.15)] overflow-hidden"
style={{ background }}
>
<motion.div
className="absolute left-5 text-white/50"
style={{ opacity: leftArrowOpacity, x: leftArrowX }}
>
<IoArrowBackOutline size={14} />
</motion.div>
<motion.div
className="absolute right-5 text-zinc-400/50"
style={{ opacity: rightArrowOpacity, x: rightArrowX }}
>
<IoArrowForwardOutline size={14} />
</motion.div>
<motion.div
ref={handleRef}
id="handle"
drag="x"
dragConstraints={{ left: 0, right: maxDrag }}
dragElastic={0}
dragMomentum={true}
onDragEnd={handleDragEnd}
className="w-[68%] h-10 bg-gradient-to-b from-white to-zinc-100 rounded-full shadow-lg cursor-pointer flex center overflow-clip absolute left-1"
style={{ x }}
onClick={handleClick}
>
<motion.div
id="handle-label"
style={{
x: labelX,
}}
className="absolute inset-0 flex justify-start items-center w-min px-2">
<div className="flex flex-row center gap-2 font-regular text-xs whitespace-nowrap w-min">
<motion.div className="w-min text-center text-zinc-500" style={{ opacity: cancelTextOpacity }}>
Slide to cancel
</motion.div>
<div className="w-[10px] flex flex-row center gap-0.5 opacity-80">
<div className="h-3 w-0.5 bg-zinc-200"></div>
<div className="h-3 w-0.5 bg-zinc-200"></div>
</div>
<motion.div className="w-min text-center text-zinc-500" style={{ opacity: confirmTextOpacity }}>
Slide to confirm
</motion.div>
</div>
</motion.div>
</motion.div>
</motion.div>
</div>
);
};
export default SlideButton;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment