Latest stories
See all 48 stories →New every Thursday
Your image grows gently inside its frame when the mouse passes over it or when the surrounding link gets keyboard focus, without a line of JavaScript.
Updated
This effect is free — the Gallery category contains 44 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
See all 48 stories →New every Thursday
21 – 23 March · Covered market · 140 publishers · free entry Sunday
ProgrammeSigningsGetting there
The .hzm-frame clips the overflow (overflow: hidden) and holds your <img class="hzm-image">, cropped like a cover photo (object-fit: cover). On hover the image goes to scale(var(--hzm-scale)), that is 1.3, over --hzm-duration (0.5 s) with a curve that slows down at the end.
By default the frame is an <a> link: replace href="#" with your address. The zoom also triggers when the link gets keyboard focus, and when a link wrapping the frame, for instance a whole article card, is hovered or reached with Tab. If the image is not clickable, change the <a> into a <div>: hover remains, focus no longer applies.
Everything is CSS tied to .fx-0179-root and the hzm- prefix: as many copies as you like on a page, no clash with your own classes. The frame takes the available width up to --hzm-width (280 px) and keeps --hzm-ratio.
:focus-visible). In Safari, Tab only reaches links when “Press Tab to highlight each item” is on (Option + Tab otherwise).aria-hidden).prefers-reduced-motion: reduce, the transition is removed; the image goes straight to its enlarged size.CSS only: transform, object-fit, aspect-ratio, :focus-visible (in a separate rule so hover does not break in browsers that lack it). Measured in Chromium 151, Firefox 153 and WebKit 26.5: two independent copies, zoom on hover and keyboard focus, reduced motion without transition, no overflow at 390 px.
Before Safari 15.4, the keyboard-focus zoom does not apply (hover does). Without aspect-ratio, the frame takes the image's height.
Copy the two blocks (HTML and CSS) into your page. No JavaScript, no dependencies.
<div class="fx-0179-root">
<a class="hzm-frame" href="#">
<img class="hzm-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='560' height='400' viewBox='0 0 560 400'%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop stop-color='%23667eea'/%3E%3Cstop offset='.5' stop-color='%23764ba2'/%3E%3Cstop offset='1' stop-color='%23f093fb'/%3E%3C/linearGradient%3E%3Cpattern id='s' width='20' height='20' patternUnits='userSpaceOnUse' patternTransform='rotate(45)'%3E%3Crect x='10' width='10' height='20' fill='%236366f1' fill-opacity='.1'/%3E%3C/pattern%3E%3Cpattern id='q' width='20' height='20' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 .5H20M.5 0V20' stroke='%23fff' stroke-opacity='.1'/%3E%3C/pattern%3E%3CradialGradient id='l' cx='168' cy='120' r='241' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%23fff' stop-opacity='.3'/%3E%3Cstop offset='1' stop-color='%23fff' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='d' cx='392' cy='280' r='241' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-opacity='.2'/%3E%3Cstop offset='1' stop-opacity='0'/%3E%3C/radialGradient%3E%3Crect width='560' height='400' fill='url(%23g)'/%3E%3Crect width='560' height='400' fill='url(%23s)'/%3E%3Crect width='560' height='400' fill='url(%23q)'/%3E%3Crect width='560' height='400' fill='url(%23l)'/%3E%3Crect width='560' height='400' fill='url(%23d)'/%3E%3C/svg%3E" alt="Violet gradient grid pattern" width="560" height="400">
<span class="hzm-label" aria-hidden="true">Hover Me</span>
</a>
</div>
/* fx-0179 — Hover Zoom: the image grows inside its frame on hover or keyboard focus (CSS only).
Everything is scoped to .fx-0179-root and the hzm- prefix: two copies on one page stay independent. */
.fx-0179-root,
.fx-0179-root *,
.fx-0179-root *::before,
.fx-0179-root *::after {
box-sizing: border-box;
}
.fx-0179-root {
--hzm-scale: 1.3; /* zoom level on hover / keyboard focus */
--hzm-duration: 0.5s; /* zoom-in and zoom-out time */
--hzm-width: 280px; /* maximum frame width (the frame shrinks on small screens) */
--hzm-ratio: 7 / 5; /* frame shape */
--hzm-radius: 0.75rem; /* frame corners */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 250px;
padding: 16px;
position: relative;
background: #0a0a0f;
color: #fff;
font-family: sans-serif;
}
/* Frame: fluid width (capped by --hzm-width), height from --hzm-ratio */
.hzm-frame {
position: relative;
display: block;
width: 100%;
max-width: var(--hzm-width);
aspect-ratio: var(--hzm-ratio);
border-radius: var(--hzm-radius);
overflow: hidden;
isolation: isolate;
background: #12121a;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.hzm-frame:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
/* Your image: any <img>, cropped to the frame like a cover photo */
.hzm-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
-webkit-user-select: none;
user-select: none;
}
/* Optional caption over the image (delete the span if you do not want it) */
.hzm-label {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
font-size: 1.2rem;
text-shadow: rgba(0, 0, 0, 0.3) 0 2px 4px;
pointer-events: none;
}
.hzm-frame {
cursor: pointer;
color: inherit;
text-decoration: none;
}
.hzm-image {
transition: transform var(--hzm-duration) cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* Hover on the frame or on a link that wraps it */
.hzm-frame:hover .hzm-image,
.fx-0179-root a:hover .hzm-image {
transform: scale(var(--hzm-scale));
}
/* Keyboard focus on the frame (a link) or on a link that wraps it (separate rule: older browsers skip it alone) */
.hzm-frame:focus-visible .hzm-image,
.fx-0179-root a:focus-visible .hzm-image {
transform: scale(var(--hzm-scale));
}
/* Reduced motion: the image still zooms, without the transition */
@media (prefers-reduced-motion: reduce) {
.hzm-image {
transition: none;
}
}
Pure CSS — no JavaScript needed.
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Image (HTML — src of .hzm-image) |
violet grid pattern (inline SVG) | Your photo and its alt, which also names the link. |
--hzm-scale |
1.3 | Enlargement on hover; 1.05 to 1.15 for cards, 1.3 for a strong effect. |
--hzm-duration |
0.5s | Duration in and out; 1s or more for a large image. |
--hzm-width · --hzm-ratio · --hzm-radius |
280px · 7 / 5 · 0.75rem | Maximum frame width (it shrinks on small screens), proportions and rounding. |
| Frame tag | <a href="#"> |
Put in your address; or use a <div> if the image is not a link, or place the frame inside a larger link (a whole card). |
.hzm-label caption |
“Hover Me” | Text centred on the image; delete the <span> on a real photo. |
No. It all lives in the CSS: :hover and :focus-visible trigger the transform, the frame clips the excess.
Wrap the frame and the text in one link placed inside .fx-0179-root: the .fx-0179-root a:hover .hzm-image rule enlarges the image as soon as the card is hovered.
Only if the file is exactly the frame's size. Use an image 1.3 times wider (or the value of --hzm-scale).