CSS Image Zoom on Hover

Smooth image zoom on hover using CSS transform and overflow:hidden.

Published May 19, 2026 Beginner 7 min read

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.

Zoom on hover

Hover to zoom
Hover to zoom

HTML

<div class="zoom-wrap">
  <img src="/img/examples/image-zoom/zoom-1.webp" alt="Hover to zoom">
</div>
<div class="zoom-wrap">
  <img src="/img/examples/image-zoom/zoom-2.webp" alt="Hover to zoom">
</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);
}

Other ways to build it

Zoom with a caption sliding in

The same overflow: hidden that clips the zoom also hides a caption bar parked just 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.

Hover to zoom and reveal the caption Caption slides up on hover
Hover to zoom and reveal the caption Clipped by the same frame

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/zoom-origin-1.webp" alt="Anchored to the bottom right corner">
  </a>
  <a href="#image-zoom-origin2" id="origin2" class="zoom-wrap zoom-origin zoom-origin-top">
    <img src="/img/examples/image-zoom/zoom-origin-2.webp" alt="Anchored to the top of the frame">
  </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; }
}

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 simply not painted. Drop overflow: hidden and the image grows over its neighbours 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.

CSS properties used

overflow
hidden on the wrapper clips everything that grows past the frame. Without it the scaled image spills over whatever sits next to it.
transform
scale() is what actually zooms. It runs on the compositor, so it does not trigger layout on the rest of the page.
transform-origin
Sets the anchor the scale grows from. center by default, which keeps the middle of the frame fixed.
object-fit
cover fills the frame and crops the excess instead of distorting the image to match the box.
transition
Animates the transform between the resting and hovered states. Without it the image snaps to the larger size.
display
block on the image removes the baseline descender gap that otherwise shows as a thin strip under the picture.

Browser support

FeatureChromeFirefoxSafariEdge
transform43.53.112
transition455.112
object-fit32361079
:focus-visible86415.486
prefers-reduced-motion746310.179

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.

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 just arrives there without the intermediate frames.

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: top keeps 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-origin to pick the corner.

Mistakes worth avoiding

  • Leaving overflow: hidden off 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: block on 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.

Frequently asked questions

How do you zoom an image on hover with CSS?
Wrap the image in a container with a fixed size and 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?
The container is missing overflow: hidden. transform: scale() does not change an element's layout box, so the browser paints the enlarged image straight over its neighbours unless something clips it.
Can I zoom toward the mouse pointer?
Not with CSS alone. The pointer's position is not exposed to the stylesheet, so 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?
Not when it is a transform. Scaling is handled by the compositor and does not force a layout or paint of the rest of the page. Animating 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?
Add a second trigger. Wrapping the frame in a link and matching :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 simply not have it.