CSS Image Zoom on Hover
A CSS image zoom scales the picture on hover while the frame holds its size. Overflow hidden plus a transform scale with a transition does it.
- Level
- Beginner
- Published
- Updated
- Reading time
- 7 min
Try it
3 live demos
Zoom on hover
Other ways to build it
Caption slides up on hover
Clipped by the same frame
Zoom with a caption sliding in
See the codeCorner anchored zoom that a keyboard can reach
See the codeBuild it
6 snippets to copy
CSS Image Zoom on Hover
HTML
<div class="zoom-wrap">
<img src="/img/examples/image-zoom/css-image-zoom-wave-bands-teal-lime.webp" srcset="/img/examples/image-zoom/css-image-zoom-wave-bands-teal-lime-400.webp 400w, /img/examples/image-zoom/css-image-zoom-wave-bands-teal-lime.webp 800w" sizes="200px" alt="Three stacked wave bands in dark gray, teal and lime across the bottom of a black field.">
</div>
<div class="zoom-wrap">
<img src="/img/examples/image-zoom/css-image-zoom-concentric-rings-glow.webp" srcset="/img/examples/image-zoom/css-image-zoom-concentric-rings-glow-400.webp 400w, /img/examples/image-zoom/css-image-zoom-concentric-rings-glow.webp 800w" sizes="200px" alt="Three concentric rings sitting over a teal and lime radial glow on black.">
</div>
CSS
/* overflow:hidden clips the image once it grows past the frame */
.zoom-wrap {
width: 200px;
height: 130px;
border-radius: 10px;
overflow: hidden;
}
.zoom-wrap img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform .4s ease;
}
.zoom-wrap:hover img {
transform: scale(1.3);
}
Zoom with a caption sliding in
The same overflow: hidden that clips the zoom also hides a caption bar parked below the bottom edge. The bar starts at translateY(100%), which puts it exactly its own height outside the frame, and hovering moves it to translateY(0). The gradient behind the caption keeps the text readable over a light photo.
HTML
<div class="zoom-wrap zoom-caption">
<img src="coast.jpg" alt="Coastline at dusk">
<span class="zoom-cap">Coastline at dusk</span>
</div>
CSS
.zoom-caption {
position: relative;
}
/* parked exactly its own height below the frame, and clipped by the
overflow:hidden that is already there for the zoom */
.zoom-cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 1.4rem .7rem .5rem;
background: linear-gradient(to top, rgba(0, 0, 0, .85), transparent);
color: #f0f0f0;
font-size: .8rem;
transform: translateY(100%);
transition: transform .4s ease;
}
.zoom-caption:hover .zoom-cap {
transform: translateY(0);
}
Corner anchored zoom that a keyboard can reach
Changing transform-origin to bottom right pins that corner, so the picture drifts up and to the left rather than growing outward from the middle. It suits an image whose subject sits off center. The frame is also an anchor here, and the rule matches :hover and :focus-visible together, so tabbing to it produces the same zoom a pointer does.
HTML
<div style="display:flex;gap:1rem;flex-wrap:wrap;justify-content:center">
<a href="#image-zoom-origin" id="origin" class="zoom-wrap zoom-origin">
<img src="/img/examples/image-zoom/css-image-zoom-corner-glow-dots.webp" srcset="/img/examples/image-zoom/css-image-zoom-corner-glow-dots-400.webp 400w, /img/examples/image-zoom/css-image-zoom-corner-glow-dots.webp 800w" sizes="200px" alt="A lime and teal radial glow in the bottom right corner with a few scattered dots on black.">
</a>
<a href="#image-zoom-origin2" id="origin2" class="zoom-wrap zoom-origin zoom-origin-top">
<img src="/img/examples/image-zoom/css-image-zoom-sky-gradient.webp" srcset="/img/examples/image-zoom/css-image-zoom-sky-gradient-400.webp 400w, /img/examples/image-zoom/css-image-zoom-sky-gradient.webp 800w" sizes="200px" alt="A sky gradient from lime at the top to teal, with a dark circle near the top and a black ground band below.">
</a>
</div>
CSS
.zoom-origin img {
transform-origin: bottom right;
}
/* one rule for both triggers, so the keyboard gets the same state
the pointer does */
.zoom-origin:hover img,
.zoom-origin:focus-visible img {
transform: scale(1.25);
}
.zoom-origin:focus-visible {
outline: 2px solid #b8ff57;
outline-offset: 3px;
}
.zoom-origin-top img {
transform-origin: top center;
}
@media (prefers-reduced-motion: reduce) {
.zoom-wrap img { transition: none; }
}
Understand it
How it works and 6 properties
A CSS image zoom on hover scales the picture up while its frame stays exactly the same size, so the surrounding layout never moves. Two declarations carry the whole effect: overflow: hidden on the wrapper, which clips whatever grows past the edge, and transform: scale() on the image with a transition to smooth it. The image element is what moves, never the wrapper.
Scaling an image is a compositor operation, so the browser can run it without recalculating layout for anything else on the page. That is the difference between this and an older approach that animated width and height, which forced a reflow on every frame and made a grid of thumbnails stutter. Two variants follow: one adds a caption that slides in from below the same clipped edge, and one changes where the zoom grows from and makes the effect reachable from the keyboard.
How it works
overflow:hidden on the container clips the image when it scales beyond its bounds. The <img> itself gets transform:scale() on :hover with a transition. Because scale() grows from the center, the effect is a smooth zoom-in without shifting the image position. The container dimensions stay fixed throughout.
The wrapper is a fixed box. It has a width, a height, a border radius, and overflow: hidden. Once the image inside it scales past 1, the parts hanging over the edge are not painted. Drop overflow: hidden and the image grows over its neighbors instead, which in a grid of thumbnails means each one covers the cards to its right as the pointer passes across.
The image is set to width: 100%; height: 100%; object-fit: cover. Without object-fit, a picture whose natural proportions differ from the frame gets squashed to fit, and faces in a portrait stretch sideways. cover scales the image until it fills the box on both axes and crops the overflow, which is almost always what a thumbnail wants. contain is the other useful value, and it letterboxes instead of cropping.
display: block on the image looks like a detail and is not. An inline image sits on the text baseline, and the browser reserves a few pixels of descender space below it, so a strip of the wrapper background shows along the bottom edge. Setting the image to block removes that gap. It is one of the most frequently reported "why is there a line under my image" bugs and has nothing to do with the zoom.
transform: scale(1.3) grows the image from its center by default, and transform-origin moves that anchor. Set to bottom right it pins the corner and pushes the rest of the picture away from it, which reads as a slow pan rather than a zoom. The transition duration is worth tuning rather than copying: under about 200ms the change reads as a jump, and past roughly 600ms the image is still moving after the pointer has left.
Accessibility notes
Hover is not a state a keyboard can enter. If the zoom is decorative that costs nothing, but the moment a caption or a link label only appears on hover, keyboard users lose it. The second variant wraps the frame in an anchor and matches on :hover, :focus-visible together, so tabbing to the thumbnail produces the same result as pointing at it.
Touchscreens have no hover either. Mobile browsers synthesise one on first tap, so the image zooms and usually stays zoomed until something else is tapped. That is harmless for a purely visual effect and a real problem when the hover state hides content the reader needs.
A zoom is motion, and a page full of thumbnails that all animate is more of it than it looks on paper. The stylesheet ends with a prefers-reduced-motion block that removes the transition. The image still changes size on hover, it arrives there without the intermediate frames.
Mistakes worth avoiding
- Leaving
overflow: hiddenoff the wrapper. The image escapes its frame on hover and paints over the cards beside it. - Scaling the wrapper instead of the image. The frame itself grows, the layout around it shifts, and the clipping never happens because the wrapper and its contents grow together.
- Omitting
object-fit: cover. Any image whose proportions differ from the frame gets stretched, which is most visible on faces. - Forgetting
display: blockon the image. A few pixels of baseline gap show as a stripe of background along the bottom edge of the frame. - Relying on hover to expose a caption or a link. Neither a keyboard nor a touchscreen can reach it, so that content is effectively missing for a large share of readers.
Ship it
5 features in 4 browsers, 5 questions
Browser support
| Feature | ||||
|---|---|---|---|---|
transform | 4 | 3.5 | 3.1 | 12 |
transition | 4 | 5 | 5.1 | 12 |
object-fit | 32 | 36 | 10 | 79 |
:focus-visible | 86 | 4 | 15.4 | 86 |
prefers-reduced-motion | 74 | 63 | 10.1 | 79 |
Figures come from Can I Use for 2D transforms, transitions, object-fit, :focus-visible and the reduced motion media query. overflow: hidden predates all of them and has no meaningful support gap. object-fit is the newest of the group by a wide margin in Safari, and where it is missing the image falls back to being stretched to the box rather than cropped, which degrades in an ugly but non-breaking way.
What you can build with it
- Product grids. A subtle zoom gives each tile a hover affordance without adding a border, a shadow, or anything that shifts the grid.
- Article and blog cards. The thumbnail zooms while the card itself stays still, so the row of cards does not reflow under the pointer.
- Portfolio thumbnails. Pairs naturally with a full size overlay. The CSS lightbox opens the large version from the same thumbnail.
- Team photos. A slow zoom with
transform-origin: topkeeps the face centered in the frame as the picture grows. - Map or diagram previews. A cropped detail zooms toward the region the caption is describing, using
transform-originto pick the corner.
Frequently asked questions
How do you zoom an image on hover with CSS?
overflow: hidden, give the image transition: transform .4s ease, then add .wrap:hover img { transform: scale(1.3) }. The container clips whatever grows past its edge, so the frame stays the same size while the picture inside it grows.Why does my image overflow its container on hover?
overflow: hidden. transform: scale() does not change an element's layout box, so the browser paints the enlarged image straight over its neighbors unless something clips it.Can I zoom toward the mouse pointer?
transform-origin can only be set to a fixed anchor such as a corner or an edge. Tracking the cursor needs a script that writes the coordinates back into a custom property.Does an image zoom hurt page performance?
width and height for the same look does force both, and that is where the stutter on thumbnail grids comes from.How do I make a hover zoom work on mobile?
:focus-visible alongside :hover covers keyboards, and a hidden checkbox with a label covers taps. For a purely decorative zoom it is reasonable to let touch devices not have it.