Lens
A lens component to zoom into images, videos, or practically anything.
Apple Vision Pro
The all new apple vision pro was the best thing that happened around 8 months ago, not anymore.
Installation
Install dependencies
npm i framer-motion clsx tailwind-merge
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/lens.tsx
"use client";
import React, { useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
interface LensProps {
children: React.ReactNode;
zoomFactor?: number;
lensSize?: number;
position?: {
x: number;
y: number;
};
isStatic?: boolean;
isFocusing?: () => void;
hovering?: boolean;
setHovering?: (hovering: boolean) => void;
}
export const Lens: React.FC<LensProps> = ({
children,
zoomFactor = 1.5,
lensSize = 170,
isStatic = false,
position = { x: 200, y: 150 },
hovering,
setHovering,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [localIsHovering, setLocalIsHovering] = useState(false);
const isHovering = hovering !== undefined ? hovering : localIsHovering;
const setIsHovering = setHovering || setLocalIsHovering;
// const [isHovering, setIsHovering] = useState(false);
const [mousePosition, setMousePosition] = useState({ x: 100, y: 100 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setMousePosition({ x, y });
};
return (
<div
ref={containerRef}
className="relative overflow-hidden rounded-lg z-20"
onMouseEnter={() => {
setIsHovering(true);
}}
onMouseLeave={() => setIsHovering(false)}
onMouseMove={handleMouseMove}
>
{children}
{isStatic ? (
<div>
<motion.div
initial={{ opacity: 0, scale: 0.58 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="absolute inset-0 overflow-hidden"
style={{
maskImage: `radial-gradient(circle ${lensSize / 2}px at ${
position.x
}px ${position.y}px, black 100%, transparent 100%)`,
WebkitMaskImage: `radial-gradient(circle ${lensSize / 2}px at ${
position.x
}px ${position.y}px, black 100%, transparent 100%)`,
transformOrigin: `${position.x}px ${position.y}px`,
}}
>
<div
className="absolute inset-0"
style={{
transform: `scale(${zoomFactor})`,
transformOrigin: `${position.x}px ${position.y}px`,
}}
>
{children}
</div>
</motion.div>
</div>
) : (
<AnimatePresence>
{isHovering && (
<div>
<motion.div
initial={{ opacity: 0, scale: 0.58 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="absolute inset-0 overflow-hidden"
style={{
maskImage: `radial-gradient(circle ${lensSize / 2}px at ${
mousePosition.x
}px ${mousePosition.y}px, black 100%, transparent 100%)`,
WebkitMaskImage: `radial-gradient(circle ${
lensSize / 2
}px at ${mousePosition.x}px ${
mousePosition.y
}px, black 100%, transparent 100%)`,
transformOrigin: `${mousePosition.x}px ${mousePosition.y}px`,
zIndex: 50,
}}
>
<div
className="absolute inset-0"
style={{
transform: `scale(${zoomFactor})`,
transformOrigin: `${mousePosition.x}px ${mousePosition.y}px`,
}}
>
{children}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
)}
</div>
);
};
Examples
Basic with Animation
Apple Vision Pro
The all new apple vision pro was the best thing that happened around 8 months ago, not anymore.
Static
Apple Vision Pro
The all new apple vision pro was the best thing that happened around 8 months ago, not anymore.
Lens on a React Component
Apple Vision Pro
The all new apple vision pro was the best thing that happened around 8 months ago, not anymore.
Props
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | Required | The content to be displayed inside the lens |
zoomFactor | number | 1.5 | The magnification factor for the lens |
lensSize | number | 170 | The diameter of the lens in pixels |
position | { x: number, y: number } | { x: 200, y: 150 } | The static position of the lens (when isStatic is true) |
isStatic | boolean | false | If true, the lens stays in a fixed position; if false, it follows the mouse |
isFocusing | () => void | - | Callback function when the lens is focusing (not used in current implementation) |
hovering | boolean | - | External control for the hover state |
setHovering | (hovering: boolean) => void | - | External setter for the hover state |