iOS Classic Slide to Unlock

April 2024

I recreated the classic 'Slide to Unlock' from the old iPhones using Framer Motion for the animation and a mix of Tailwind CSS and inline CSS for styling. I've also added the unlock sound to make it feel more authentic (works best on Chrome desktop). Wish I still had my old iPhone 3G to make it even more accurate!

slide to unlock
import { motion, useMotionValue, useTransform } from "framer-motion";
import useAudio from "hooks/useAudio";
import { useRef } from "react";
const sliderWidth = 300;
const handleWidth = 75;
const padding = 4;
export default function iOSClassicSlideToUnlock() {
const sliderRef = useRef<HTMLDivElement>(null);
const dragPosition = useMotionValue(0);
const unlockSound = useAudio({ src: "/lab/slideToUnlockSound.mp3" });
const maxDragLimit = sliderWidth - handleWidth - padding * 2;
const activeWidth = useTransform(dragPosition, (x) => {
if (x <= handleWidth) {
return 0;
}
return x + handleWidth;
});
const textVisibility = useTransform(dragPosition, [0, maxDragLimit], [1, 0]);
const handleDragEnd = () => {
if (dragPosition.get() >= maxDragLimit) {
unlockSound?.play();
}
};
return (
<>
<div
className="relative flex h-[56px] items-center overflow-hidden rounded-xl"
ref={sliderRef}
style={{
padding: padding,
background: "linear-gradient(180deg, #101010, #302f34 150%)",
width: sliderWidth,
boxShadow:
"inset 0px 15px 20px rgb(0 0 0 / 50%), inset 0 0 0 1px #4C4B50",
}}
>
<motion.div
className="absolute inset-[1px] z-[1] rounded-2xl"
style={{
width: activeWidth,
background: "linear-gradient(180deg, #101010, #302f34 150%)",
boxShadow: "inset 0px 16px 20px rgb(0 0 0 / 50%)",
}}
/>
<motion.div
className="z-[1] flex h-[48px] items-center justify-center rounded-lg"
drag="x"
dragConstraints={{
left: 0,
right: sliderWidth - handleWidth - padding * 2,
}}
dragElastic={0}
style={{
width: handleWidth,
background: "linear-gradient(180deg, #F1F1F1 50%, #C4C4C4 50%)",
x: dragPosition,
}}
dragSnapToOrigin
dragTransition={{ bounceStiffness: 100, bounceDamping: 20 }}
onDragEnd={handleDragEnd}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={44}
height={36}
viewBox="0 0 44 36"
fill="none"
className="h-9 w-9"
>
<defs>
<linearGradient
id="arrow-gradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop
offset="50%"
style={{ stopColor: "#AEAEAE", stopOpacity: 1 }}
/>
<stop
offset="50%"
style={{ stopColor: "#888888", stopOpacity: 1 }}
/>
</linearGradient>
<filter id="arrow-inset-shadow">
<feOffset dx="0" dy="1" />
<feGaussianBlur stdDeviation="1" result="offset-blur" />
<feComposite
operator="out"
in="SourceGraphic"
in2="offset-blur"
result="inverse"
/>
<feFlood floodColor="black" floodOpacity="1" result="color" />
<feComposite
operator="in"
in="color"
in2="inverse"
result="shadow"
/>
<feComposite operator="over" in="shadow" in2="SourceGraphic" />
</filter>
</defs>
<path
fill="url(#arrow-gradient)"
filter="url(#arrow-inset-shadow)"
d="M21.98 0v10H0v16h21.98v10L44 18.122z"
/>
</svg>
</motion.div>
<motion.div
className="absolute left-0 w-full select-none text-[26px] text-white"
style={{
paddingRight: "20px",
paddingLeft: handleWidth + 20, // paddingRight
background:
"linear-gradient(to right, #585858 0, white 10%, #585858 20%)",
backgroundPosition: "0",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animation: "shine 2.8s infinite linear",
animationFillMode: "forwards",
opacity: textVisibility,
}}
>
slide to unlock
</motion.div>
</div>
<style jsx>{`
@keyframes shine {
0% {
background-position: 0;
}
60% {
background-position: ${sliderWidth}px;
}
100% {
background-position: ${sliderWidth}px;
}
}
`}</style>
</>
);
}