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 (<><divclassName="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.divclassName="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.divclassName="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,}}dragSnapToOrigindragTransition={{ bounceStiffness: 100, bounceDamping: 20 }}onDragEnd={handleDragEnd}><svgxmlns="http://www.w3.org/2000/svg"width={44}height={36}viewBox="0 0 44 36"fill="none"className="h-9 w-9"><defs><linearGradientid="arrow-gradient"x1="0%"y1="0%"x2="0%"y2="100%"><stopoffset="50%"style={{ stopColor: "#AEAEAE", stopOpacity: 1 }}/><stopoffset="50%"style={{ stopColor: "#888888", stopOpacity: 1 }}/></linearGradient><filter id="arrow-inset-shadow"><feOffset dx="0" dy="1" /><feGaussianBlur stdDeviation="1" result="offset-blur" /><feCompositeoperator="out"in="SourceGraphic"in2="offset-blur"result="inverse"/><feFlood floodColor="black" floodOpacity="1" result="color" /><feCompositeoperator="in"in="color"in2="inverse"result="shadow"/><feComposite operator="over" in="shadow" in2="SourceGraphic" /></filter></defs><pathfill="url(#arrow-gradient)"filter="url(#arrow-inset-shadow)"d="M21.98 0v10H0v16h21.98v10L44 18.122z"/></svg></motion.div><motion.divclassName="absolute left-0 w-full select-none text-[26px] text-white"style={{paddingRight: "20px",paddingLeft: handleWidth + 20, // paddingRightbackground:"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></>);}