Instantly share code, notes, and snippets.
Last active
November 27, 2024 14:23
-
Star
0
(0)
You must be signed in to star a gist -
Fork
1
(1)
You must be signed in to fork a gist
-
Save 0xsommer/ad252f847ad5826ee2a174674760de47 to your computer and use it in GitHub Desktop.
Button: Slide to confirm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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