Weekend in Lisbon
Three days of tiles, tarts and trams.
The photo zooms in toward the part you point at, and the zoom follows as you move, like a product gallery. A 3x3 grid of invisible cells moves transform-origin, so no JavaScript and no second image.
Also known as: product image zoom on hover, zoom image where mouse points, hover magnify image css.
<a class="hfx-image-zoom-to-cursor" href="#" aria-label="Open the full size photo"> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-cell" aria-hidden="true"></span> <span class="hfx-image-zoom-to-cursor-photo"></span> <span class="hfx-image-zoom-to-cursor-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2M11 8.5v5M8.5 11h5"/></svg></span> </a>
.hfx-image-zoom-to-cursor {
--c: var(--hfx-color, #3b82f6);
font-size: var(--hfx-size, 16px);
position: relative;
isolation: isolate;
display: block;
width: 16em;
height: 10em;
overflow: hidden;
border-radius: 0.75em;
cursor: zoom-in;
}
.hfx-image-zoom-to-cursor-cell {
position: absolute;
z-index: 3;
top: 0;
left: 0;
width: calc(100% / 3);
height: calc(100% / 3);
}
.hfx-image-zoom-to-cursor-cell:nth-child(3n + 2) { left: calc(100% / 3); }
.hfx-image-zoom-to-cursor-cell:nth-child(3n) { left: calc(200% / 3); }
.hfx-image-zoom-to-cursor-cell:nth-child(n + 4) { top: calc(100% / 3); }
.hfx-image-zoom-to-cursor-cell:nth-child(n + 7) { top: calc(200% / 3); }
.hfx-image-zoom-to-cursor-photo {
position: absolute;
inset: 0;
background: radial-gradient(75% 42% at 18% 112%, color-mix(in srgb, var(--c) 45%, #0f172a) 99%, transparent),
radial-gradient(66% 36% at 92% 108%, color-mix(in srgb, var(--c) 60%, #0f172a) 99%, transparent),
conic-gradient(from 163deg at 6% 61%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 10% 57%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 14% 62%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 31% 63%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 35% 60%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 72% 64%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 77% 59%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 81% 62%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 163deg at 96% 60%, transparent 0, color-mix(in srgb, var(--c) 38%, #06101f) 0.4deg 33.6deg, transparent 34deg),
conic-gradient(from 128deg at 70% 42%, transparent 0, color-mix(in srgb, var(--c) 78%, #1e293b) 0.4deg 103.6deg, transparent 104deg),
conic-gradient(from 120deg at 92% 55%, transparent 0, color-mix(in srgb, var(--c) 78%, #1e293b) 0.4deg 119.6deg, transparent 120deg),
conic-gradient(from 124deg at 28% 38%, transparent 0, color-mix(in srgb, var(--c) 45%, #fff) 0.4deg 111.6deg, transparent 112deg),
conic-gradient(from 126deg at 50% 52%, transparent 0, color-mix(in srgb, var(--c) 45%, #fff) 0.4deg 107.6deg, transparent 108deg),
radial-gradient(circle at 70% 26%, #fff 0 0.75em, rgb(255 255 255 / 0.45) 0.85em, transparent 3em),
linear-gradient(color-mix(in srgb, var(--c) 60%, #fff), color-mix(in srgb, var(--c) 14%, #fff7ed) 75%);
transform-origin: 50% 50%;
transition: transform 0.45s cubic-bezier(0.25, 0.8, 0.25, 1), transform-origin 0.45s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.hfx-image-zoom-to-cursor-badge {
position: absolute;
right: 0.6em;
bottom: 0.6em;
z-index: 1;
display: grid;
place-items: center;
width: 2em;
height: 2em;
border-radius: 50%;
background: rgb(255 255 255 / 0.9);
color: var(--c);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.hfx-image-zoom-to-cursor-badge svg {
width: 1.1em;
height: 1.1em;
}
.hfx-image-zoom-to-cursor:is(:hover, :focus-visible) .hfx-image-zoom-to-cursor-photo {
transform: scale(2);
}
.hfx-image-zoom-to-cursor:is(:hover, :focus-visible) .hfx-image-zoom-to-cursor-badge {
opacity: 0;
transform: scale(0.6);
}
.hfx-image-zoom-to-cursor-cell:nth-child(1):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 12% 12%; }
.hfx-image-zoom-to-cursor-cell:nth-child(2):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 50% 12%; }
.hfx-image-zoom-to-cursor-cell:nth-child(3):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 88% 12%; }
.hfx-image-zoom-to-cursor-cell:nth-child(4):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 12% 50%; }
.hfx-image-zoom-to-cursor-cell:nth-child(5):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 50% 50%; }
.hfx-image-zoom-to-cursor-cell:nth-child(6):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 88% 50%; }
.hfx-image-zoom-to-cursor-cell:nth-child(7):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 12% 88%; }
.hfx-image-zoom-to-cursor-cell:nth-child(8):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 50% 88%; }
.hfx-image-zoom-to-cursor-cell:nth-child(9):hover ~ .hfx-image-zoom-to-cursor-photo { transform-origin: 88% 88%; }
@media (prefers-reduced-motion: reduce) {
.hfx-image-zoom-to-cursor, .hfx-image-zoom-to-cursor *, .hfx-image-zoom-to-cursor::before, .hfx-image-zoom-to-cursor::after, .hfx-image-zoom-to-cursor *::before, .hfx-image-zoom-to-cursor *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--hfx-color on the element or a parent.
Currently previewing #3b82f6.--hfx-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.