UI Components Medium
Image Zoom / Magnifier
A high-performance image zooming component with a magnifier lens effect. Ideal for product galleries and photography showcases.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--zoom-primary: #8b5cf6;
--zoom-primary-rgb: 139, 92, 246;
--zoom-bg: #0f172a;
--zoom-text: #f8fafc;
--zoom-border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, sans-serif;
background-color: var(--zoom-bg);
color: var(--zoom-text);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.zoom-wrapper {
display: flex;
flex-direction: column;
gap: 2rem;
width: 100%;
max-width: 1100px;
}
.zoom-header {
text-align: center;
}
.zoom-header h1 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #fff 0%, #a78bfa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.zoom-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.zoom-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
/* โโ Magnifier Target โโโโโโโโโโโโโโโโโโโโโโโ */
.zoom-target {
position: relative;
width: 100%;
border-radius: 20px;
overflow: hidden;
cursor: crosshair;
aspect-ratio: 4 / 5;
background: #1e293b;
border: 1px solid var(--zoom-border);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.zoom-target img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: opacity 0.3s ease;
}
.magnifier-lens {
position: absolute;
width: 150px;
height: 150px;
border: 2px solid #fff;
border-radius: 50%;
background: rgba(var(--zoom-primary-rgb), 0.1);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.4), 0 0 20px rgba(0, 0, 0, 0.5);
pointer-events: none;
opacity: 0;
transform: translate(-50%, -50%) scale(0.5);
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 10;
}
.zoom-target:hover .magnifier-lens {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
/* โโ Magnifier Result โโโโโโโโโโโโโโโโโโโโโโโ */
.zoom-result {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 20px;
border: 1px solid var(--zoom-border);
background-color: #1e293b;
background-repeat: no-repeat;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 0, 0, 0.4);
position: relative;
overflow: hidden;
opacity: 0;
transform: translateX(20px);
transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.2, 0, 0.2, 1);
}
.zoom-result.active {
opacity: 1;
transform: translateX(0);
}
.zoom-result::after {
content: "ZOOM PREVIEW";
position: absolute;
top: 1rem;
left: 1rem;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
padding: 4px 10px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
color: var(--zoom-primary);
border: 1px solid rgba(var(--zoom-primary-rgb), 0.3);
}
/* โโ Responsive โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
@media (max-width: 900px) {
.zoom-grid {
grid-template-columns: 1fr;
}
.zoom-target {
max-width: 500px;
margin: 0 auto;
}
.zoom-result {
max-width: 500px;
margin: 0 auto;
transform: translateY(20px);
}
.zoom-result.active {
transform: translateY(0);
}
}class ImageMagnifier {
constructor(targetElement) {
this.target = targetElement;
this.img = targetElement.querySelector("img");
this.lens = targetElement.querySelector(".js-zoom-lens");
this.result = document.querySelector(targetElement.dataset.result);
this.zoomImage = this.img.dataset.zoom || this.img.src;
if (!this.img || !this.lens || !this.result) return;
this.isActive = false;
this.cx = 0;
this.cy = 0;
this.init();
}
init() {
// We must wait for image to load to get dimensions
if (this.img.complete) {
this.setup();
} else {
this.img.addEventListener("load", () => this.setup());
}
// Set high-res background
this.result.style.backgroundImage = `url('${this.zoomImage}')`;
// Event Bindings
this.target.addEventListener("mousemove", (e) => this.move(e));
this.target.addEventListener("mouseenter", () => this.enable());
this.target.addEventListener("mouseleave", () => this.disable());
// Support touch
this.target.addEventListener("touchmove", (e) => this.move(e), { passive: false });
this.target.addEventListener("touchstart", () => this.enable());
this.target.addEventListener("touchend", () => this.disable());
// Window resize handling
window.addEventListener("resize", () => this.setup());
}
setup() {
/* Calculate the ratio between result DIV and lens: */
this.cx = this.result.offsetWidth / this.lens.offsetWidth;
this.cy = this.result.offsetHeight / this.lens.offsetHeight;
/* Set background size for the result DIV: */
const width = this.img.offsetWidth;
const height = this.img.offsetHeight;
this.result.style.backgroundSize = `${width * this.cx}px ${height * this.cy}px`;
}
enable() {
this.isActive = true;
this.result.classList.add("active");
}
disable() {
this.isActive = false;
this.result.classList.remove("active");
}
move(e) {
if (!this.isActive) return;
// Prevent scrolling on touch
if (e.type === "touchmove") e.preventDefault();
const rect = this.img.getBoundingClientRect();
// Get cursor position relative to image
let x = (e.pageX || e.touches[0].pageX) - rect.left - window.scrollX;
let y = (e.pageY || e.touches[0].pageY) - rect.top - window.scrollY;
// Constrain lens within image boundaries
const halfWidth = this.lens.offsetWidth / 2;
const halfHeight = this.lens.offsetHeight / 2;
if (x > this.img.offsetWidth) x = this.img.offsetWidth;
if (x < 0) x = 0;
if (y > this.img.offsetHeight) y = this.img.offsetHeight;
if (y < 0) y = 0;
// Update Lens Position (centered on cursor)
this.lens.style.left = `${x}px`;
this.lens.style.top = `${y}px`;
// Update Result Background Position
// The calculation needs to offset by half the lens to center the view
const bgX = x * this.cx - this.result.offsetWidth / 2;
const bgY = y * this.cy - this.result.offsetHeight / 2;
this.result.style.backgroundPosition = `-${bgX}px -${bgY}px`;
}
}
// Instantiate all magnifiers on the page
function initMagnifiers() {
const targets = document.querySelectorAll(".js-zoom-target");
targets.forEach((target) => new ImageMagnifier(target));
}
// Startup
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initMagnifiers);
} else {
initMagnifiers();
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Magnifier Zoom โ StealThis</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="zoom-wrapper">
<div class="zoom-header">
<h1>Magnifier Zoom</h1>
<p>Hover over the high-res imagery to explore details</p>
</div>
<div class="zoom-grid">
<div class="zoom-target js-zoom-target" data-result="#zoom-result-1">
<img src="https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=1600&q=90"
data-zoom="https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=3200&q=100"
alt="Premium Watch Product" />
<div class="magnifier-lens js-zoom-lens"></div>
</div>
<div id="zoom-result-1" class="zoom-result js-zoom-result"></div>
</div>
<div class="zoom-header" style="margin-top: 4rem;">
<p style="font-size: 0.9rem; opacity: 0.5;">
Advanced vanilla JavaScript implementation with modular instance support.
</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useRef, useCallback } from "react";
const IMAGES = [
{ label: "Code snippet", colors: ["#0d1117", "#161b22", "#21262d"], accent: "#58a6ff" },
{ label: "UI design", colors: ["#1a0533", "#2d1b69", "#553c9a"], accent: "#bc8cff" },
{ label: "Data chart", colors: ["#021d1a", "#033028", "#065f46"], accent: "#7ee787" },
];
function MockImage({ img }: { img: (typeof IMAGES)[0] }) {
return (
<div className="w-full h-full flex flex-col gap-2 p-4" style={{ background: img.colors[0] }}>
<div className="flex gap-1.5 mb-1">
{["#f85149", "#f1e05a", "#7ee787"].map((c) => (
<div key={c} className="w-2.5 h-2.5 rounded-full" style={{ background: c }} />
))}
</div>
{[90, 70, 80, 60, 85].map((w, i) => (
<div
key={i}
className="h-1.5 rounded-full"
style={{
width: `${w}%`,
background: i % 2 === 0 ? img.accent : img.colors[2],
opacity: 0.7,
}}
/>
))}
<div className="mt-2 grid grid-cols-3 gap-1.5">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-6 rounded"
style={{ background: img.colors[1], border: `1px solid ${img.accent}30` }}
/>
))}
</div>
</div>
);
}
function ZoomCard({ img }: { img: (typeof IMAGES)[0] }) {
const [mouse, setMouse] = useState<{ x: number; y: number } | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const LENS = 80; // lens diameter px
const ZOOM = 2.5;
const onMove = useCallback((e: React.MouseEvent) => {
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
setMouse({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}, []);
const lensX = mouse
? Math.min(Math.max(mouse.x - LENS / 2, 0), (containerRef.current?.offsetWidth ?? 300) - LENS)
: 0;
const lensY = mouse
? Math.min(Math.max(mouse.y - LENS / 2, 0), (containerRef.current?.offsetHeight ?? 200) - LENS)
: 0;
return (
<div>
<p className="text-[#8b949e] text-xs mb-1.5">{img.label}</p>
<div
ref={containerRef}
className="relative rounded-xl overflow-hidden border border-[#30363d] cursor-crosshair"
style={{ height: 140 }}
onMouseMove={onMove}
onMouseLeave={() => setMouse(null)}
>
<MockImage img={img} />
{mouse && (
<>
{/* Lens highlight */}
<div
className="absolute rounded-full border-2 border-white/50 pointer-events-none z-10"
style={{
width: LENS,
height: LENS,
left: lensX,
top: lensY,
boxShadow: "0 0 0 9999px rgba(0,0,0,0.4)",
}}
/>
{/* Zoomed preview */}
<div
className="absolute bottom-2 right-2 rounded-lg border-2 border-white/20 overflow-hidden z-20 shadow-xl pointer-events-none"
style={{ width: 100, height: 100 }}
>
<div
style={{
width: `${(containerRef.current?.offsetWidth ?? 300) * ZOOM}px`,
height: `${(containerRef.current?.offsetHeight ?? 140) * ZOOM}px`,
transform: `translate(-${mouse.x * ZOOM - 50}px, -${mouse.y * ZOOM - 50}px)`,
}}
>
<div
style={{
width: containerRef.current?.offsetWidth,
height: containerRef.current?.offsetHeight,
transform: `scale(${ZOOM})`,
transformOrigin: "top left",
}}
>
<MockImage img={img} />
</div>
</div>
</div>
</>
)}
</div>
</div>
);
}
export default function ZoomImageRC() {
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="w-full max-w-sm space-y-4">
<h2 className="text-[#e6edf3] font-bold text-lg mb-4">Zoom / Magnifier</h2>
{IMAGES.map((img) => (
<ZoomCard key={img.label} img={img} />
))}
<p className="text-[11px] text-center text-[#484f58]">Hover over an image to magnify</p>
</div>
</div>
);
}<script setup>
import { ref, reactive } from "vue";
const IMAGES = [
{ label: "Code snippet", colors: ["#0d1117", "#161b22", "#21262d"], accent: "#58a6ff" },
{ label: "UI design", colors: ["#1a0533", "#2d1b69", "#553c9a"], accent: "#bc8cff" },
{ label: "Data chart", colors: ["#021d1a", "#033028", "#065f46"], accent: "#7ee787" },
];
const LENS = 80;
const ZOOM = 2.5;
const DOT_COLORS = ["#f85149", "#f1e05a", "#7ee787"];
const LINE_WIDTHS = [90, 70, 80, 60, 85];
const mouseStates = reactive(IMAGES.map(() => null));
const containerEls = ref([]);
function setContainerRef(el, idx) {
if (el) containerEls.value[idx] = el;
}
function onMove(idx, e) {
const rect = containerEls.value[idx]?.getBoundingClientRect();
if (!rect) return;
mouseStates[idx] = { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
function onLeave(idx) {
mouseStates[idx] = null;
}
function getLensX(idx) {
const m = mouseStates[idx];
if (!m) return 0;
return Math.min(
Math.max(m.x - LENS / 2, 0),
(containerEls.value[idx]?.offsetWidth ?? 300) - LENS
);
}
function getLensY(idx) {
const m = mouseStates[idx];
if (!m) return 0;
return Math.min(
Math.max(m.y - LENS / 2, 0),
(containerEls.value[idx]?.offsetHeight ?? 200) - LENS
);
}
function getContainerWidth(idx) {
return containerEls.value[idx]?.offsetWidth ?? 300;
}
function getContainerHeight(idx) {
return containerEls.value[idx]?.offsetHeight ?? 140;
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-sm space-y-4">
<h2 class="text-[#e6edf3] font-bold text-lg mb-4">Zoom / Magnifier</h2>
<div v-for="(img, idx) in IMAGES" :key="img.label">
<p class="text-[#8b949e] text-xs mb-1.5">{{ img.label }}</p>
<div
:ref="(el) => setContainerRef(el, idx)"
class="relative rounded-xl overflow-hidden border border-[#30363d] cursor-crosshair"
style="height: 140px"
@mousemove="(e) => onMove(idx, e)"
@mouseleave="onLeave(idx)"
>
<!-- MockImage -->
<div class="w-full h-full flex flex-col gap-2 p-4" :style="{ background: img.colors[0] }">
<div class="flex gap-1.5 mb-1">
<div v-for="c in DOT_COLORS" :key="c" class="w-2.5 h-2.5 rounded-full" :style="{ background: c }" />
</div>
<div
v-for="(w, i) in LINE_WIDTHS"
:key="i"
class="h-1.5 rounded-full"
:style="{ width: w + '%', background: i % 2 === 0 ? img.accent : img.colors[2], opacity: 0.7 }"
/>
<div class="mt-2 grid grid-cols-3 gap-1.5">
<div
v-for="n in 3"
:key="n"
class="h-6 rounded"
:style="{ background: img.colors[1], border: `1px solid ${img.accent}30` }"
/>
</div>
</div>
<template v-if="mouseStates[idx]">
<!-- Lens highlight -->
<div
class="absolute rounded-full border-2 border-white/50 pointer-events-none z-10"
:style="{
width: LENS + 'px',
height: LENS + 'px',
left: getLensX(idx) + 'px',
top: getLensY(idx) + 'px',
boxShadow: '0 0 0 9999px rgba(0,0,0,0.4)',
}"
/>
<!-- Zoomed preview -->
<div
class="absolute bottom-2 right-2 rounded-lg border-2 border-white/20 overflow-hidden z-20 shadow-xl pointer-events-none"
style="width: 100px; height: 100px"
>
<div
:style="{
width: getContainerWidth(idx) * ZOOM + 'px',
height: getContainerHeight(idx) * ZOOM + 'px',
transform: `translate(-${mouseStates[idx].x * ZOOM - 50}px, -${mouseStates[idx].y * ZOOM - 50}px)`,
}"
>
<div
:style="{
width: getContainerWidth(idx) + 'px',
height: getContainerHeight(idx) + 'px',
transform: `scale(${ZOOM})`,
transformOrigin: 'top left',
}"
>
<div class="w-full h-full flex flex-col gap-2 p-4" :style="{ background: img.colors[0] }">
<div class="flex gap-1.5 mb-1">
<div v-for="c in DOT_COLORS" :key="c" class="w-2.5 h-2.5 rounded-full" :style="{ background: c }" />
</div>
<div
v-for="(w, i) in LINE_WIDTHS"
:key="i"
class="h-1.5 rounded-full"
:style="{ width: w + '%', background: i % 2 === 0 ? img.accent : img.colors[2], opacity: 0.7 }"
/>
<div class="mt-2 grid grid-cols-3 gap-1.5">
<div
v-for="n in 3"
:key="n"
class="h-6 rounded"
:style="{ background: img.colors[1], border: `1px solid ${img.accent}30` }"
/>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<p class="text-[11px] text-center text-[#484f58]">Hover over an image to magnify</p>
</div>
</div>
</template><script>
const IMAGES = [
{ label: "Code snippet", colors: ["#0d1117", "#161b22", "#21262d"], accent: "#58a6ff" },
{ label: "UI design", colors: ["#1a0533", "#2d1b69", "#553c9a"], accent: "#bc8cff" },
{ label: "Data chart", colors: ["#021d1a", "#033028", "#065f46"], accent: "#7ee787" },
];
const LENS = 80;
const ZOOM = 2.5;
const DOT_COLORS = ["#f85149", "#f1e05a", "#7ee787"];
const LINE_WIDTHS = [90, 70, 80, 60, 85];
let mouseStates = IMAGES.map(() => null);
let containerEls = [];
function onMove(idx, e) {
const rect = containerEls[idx]?.getBoundingClientRect();
if (!rect) return;
mouseStates[idx] = { x: e.clientX - rect.left, y: e.clientY - rect.top };
mouseStates = [...mouseStates];
}
function onLeave(idx) {
mouseStates[idx] = null;
mouseStates = [...mouseStates];
}
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
</script>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-sm space-y-4">
<h2 class="text-[#e6edf3] font-bold text-lg mb-4">Zoom / Magnifier</h2>
{#each IMAGES as img, idx}
<div>
<p class="text-[#8b949e] text-xs mb-1.5">{img.label}</p>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={containerEls[idx]}
class="relative rounded-xl overflow-hidden border border-[#30363d] cursor-crosshair"
style="height: 140px;"
on:mousemove={(e) => onMove(idx, e)}
on:mouseleave={() => onLeave(idx)}
>
<!-- MockImage -->
<div class="w-full h-full flex flex-col gap-2 p-4" style="background: {img.colors[0]}">
<div class="flex gap-1.5 mb-1">
{#each DOT_COLORS as c}
<div class="w-2.5 h-2.5 rounded-full" style="background: {c}" />
{/each}
</div>
{#each LINE_WIDTHS as w, i}
<div class="h-1.5 rounded-full" style="width: {w}%; background: {i % 2 === 0 ? img.accent : img.colors[2]}; opacity: 0.7;" />
{/each}
<div class="mt-2 grid grid-cols-3 gap-1.5">
{#each [1, 2, 3] as _}
<div class="h-6 rounded" style="background: {img.colors[1]}; border: 1px solid {img.accent}30;" />
{/each}
</div>
</div>
{#if mouseStates[idx]}
<!-- Lens highlight -->
<div
class="absolute rounded-full border-2 border-white/50 pointer-events-none z-10"
style="width: {LENS}px; height: {LENS}px; left: {clamp(mouseStates[idx].x - LENS / 2, 0, (containerEls[idx]?.offsetWidth ?? 300) - LENS)}px; top: {clamp(mouseStates[idx].y - LENS / 2, 0, (containerEls[idx]?.offsetHeight ?? 200) - LENS)}px; box-shadow: 0 0 0 9999px rgba(0,0,0,0.4);"
/>
<!-- Zoomed preview -->
<div
class="absolute bottom-2 right-2 rounded-lg border-2 border-white/20 overflow-hidden z-20 shadow-xl pointer-events-none"
style="width: 100px; height: 100px;"
>
<div
style="width: {(containerEls[idx]?.offsetWidth ?? 300) * ZOOM}px; height: {(containerEls[idx]?.offsetHeight ?? 140) * ZOOM}px; transform: translate(-{mouseStates[idx].x * ZOOM - 50}px, -{mouseStates[idx].y * ZOOM - 50}px);"
>
<div style="width: {containerEls[idx]?.offsetWidth ?? 300}px; height: {containerEls[idx]?.offsetHeight ?? 140}px; transform: scale({ZOOM}); transform-origin: top left;">
<div class="w-full h-full flex flex-col gap-2 p-4" style="background: {img.colors[0]}">
<div class="flex gap-1.5 mb-1">
{#each DOT_COLORS as c}
<div class="w-2.5 h-2.5 rounded-full" style="background: {c}" />
{/each}
</div>
{#each LINE_WIDTHS as w, i}
<div class="h-1.5 rounded-full" style="width: {w}%; background: {i % 2 === 0 ? img.accent : img.colors[2]}; opacity: 0.7;" />
{/each}
<div class="mt-2 grid grid-cols-3 gap-1.5">
{#each [1, 2, 3] as _}
<div class="h-6 rounded" style="background: {img.colors[1]}; border: 1px solid {img.accent}30;" />
{/each}
</div>
</div>
</div>
</div>
</div>
{/if}
</div>
</div>
{/each}
<p class="text-[11px] text-center text-[#484f58]">Hover over an image to magnify</p>
</div>
</div>Image Zoom / Magnifier
Enhance your image galleries with a premium magnifier effect. This component allows users to inspect details without leaving the current view.
Features
- Smooth cursor-following magnifier lens
- Adjustable zoom level
- Borderless or styled lens options
- Performance optimized for high-resolution images
- Hardware accelerated transitions