9 CSS Image Hover Effects — One Line, Copy-Paste
These nine css image hover effects each run on a single wrapped image and swap in a zoom, a color shift, or a sliding caption the instant a cursor lands,
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Zoom in
- 02 Grayscale to color
- 03 Caption slide up
- 04 Overlay fade
- 05 3D tilt
- 06 Brightness sweep
- 07 Clip-path circle zoom
- 08 Two-image cross-fade
- 09 Shake
The nine below move from the plainest single-property change to the most layered one. 01 through 04 touch just one thing — scale, filter, position, or opacity — on hover alone. 05 and 06 add a second dimension: a tilted perspective and a moving gradient band. 07 through 09 are the idea-form entries: a clip-path reveal, a two-image swap, and a startle shake. Watch the grid above loop through all nine hover states on its own: each thumbnail sits inside a fixed, non-scrollable iframe with nothing to click, so it plays that state automatically rather than waiting on a pointer that iframe can never receive. Every "photo" in the demos below is a two-tone CSS gradient landscape with a sun and a hill layered on top, not an image file. Dropping in a real <img> or a photographic background keeps every rule working unchanged. Every one of the nine also mirrors its :hover state onto :focus-visible and :active, and each carries an @media (hover: none) fallback that fires the same end state on a tap. As a result, keyboard focus and touch screens land on the identical effect a mouse would trigger.
01Zoom in
Overflow is clipped on the wrapper while only the inner image scales up on :hover, so the frame stays a fixed size and just the photo appears to zoom closer. It suits product thumbnails and portfolio tiles where the card edge and shadow need to hold still while the picture does the moving.
.photo {
overflow: hidden;
}
.photo__pic {
transform: scale(1);
transition: transform $duration $easing;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
transform: scale(1.14);
}
@media (hover: none) {
.photo:active .photo__pic { transform: scale(1.14); }
}
02Grayscale to color
A grayscale(1) filter strips the photo's color at rest, then transitions to grayscale(0) on hover so the original hue fades back in over the wrapper's declared duration. Team photo grids and gallery filter cards use it to mark which tile is currently in focus without any extra markup.
.photo__pic {
filter: grayscale(1);
transition: filter $duration $easing;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
filter: grayscale(0);
}
@media (hover: none) {
.photo:active .photo__pic { filter: grayscale(0); }
}
03Caption slide up
A caption block sits translated fully out of view below the photo and slides to translateY(0) on hover, revealing a title and description that were never visible until then. Blog card lists and magazine-style grids use it so the caption never crowds the image at rest.
.photo__cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
transform: translateY(100%);
transition: transform $duration $easing;
}
.photo:hover .photo__cap,
.photo:focus-visible .photo__cap,
.photo:active .photo__cap {
transform: translateY(0);
}
04Overlay fade
A tinted layer positioned over the whole photo sits at opacity 0 until hover pushes it to 1, then fades back out the moment the cursor leaves. Hero background photos and category cover images use the same trick to drop a readable label over busy artwork only when someone is actually looking at that tile.
.photo__overlay {
position: absolute;
inset: 0;
background: rgba($color, .82);
opacity: 0;
transition: opacity $duration $easing;
}
.photo:hover .photo__overlay,
.photo:focus-visible .photo__overlay,
.photo:active .photo__overlay {
opacity: 1;
}
053D tilt
Inside a parent carrying perspective, the photo itself tilts on rotateX and rotateY while lifting slightly on the Y axis, so the card reads as a physical object rather than a flat rectangle. Product showcase cards and certificate or badge images benefit most, since a framed object looks right in three dimensions where a flat photo usually doesn't.
.frame {
perspective: 700px;
}
.photo {
transform-style: preserve-3d;
transform: rotateX(0) rotateY(0);
transition: transform $duration $easing;
}
.photo:hover,
.photo:focus-visible,
.photo:active {
transform: rotateX(-9deg) rotateY(11deg) translateY(-4px);
}
06Brightness sweep
A diagonal linear-gradient band sits off-screen at rest and translates across the full width of the photo on hover, mimicking a beam of light passing once over a glossy surface. Premium product cards and badge-highlight images use the sweep as a one-shot flourish rather than a toggled state.
.photo__sweep {
position: absolute;
inset: 0;
background: linear-gradient(100deg, transparent 30%, rgba($subject-cream, .65) 50%, transparent 70%);
transform: translateX(-120%);
pointer-events: none;
}
.photo:hover .photo__sweep,
.photo:focus-visible .photo__sweep,
.photo:active .photo__sweep {
transform: translateX(120%);
transition: transform $duration $easing;
}
07Clip-path circle zoom
clip-path:circle() starts at a 0% radius and grows to 150% on hover, so a circular window expands out from the center until it covers the whole photo sitting underneath a dimmer base layer. Event teaser images and spotlight-reveal cards use the growing circle to hide most of the picture until someone actually commits to looking.
.photo__pic {
clip-path: circle(0% at 50% 50%);
transition: clip-path $duration $easing;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
clip-path: circle(150% at 50% 50%);
}
08Two-image cross-fade
Two photos are stacked in the exact same spot with position:absolute, and the second transitions from opacity 0 to 1 on hover, replacing the scene shown without either image ever moving. Before/after product shots and color-option previews rely on the swap landing in place rather than sliding, since a moving comparison is harder to read at a glance.
.photo__b {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity $duration $easing;
}
.photo:hover .photo__b,
.photo:focus-visible .photo__b,
.photo:active .photo__b {
opacity: 1;
}
09Shake
A short @keyframes animation nudges the photo left and right by a handful of pixels in quick, uneven steps that settle back to translateX(0) before the animation ends. It fits product images carrying a limited-sale badge or any banner photo that needs to interrupt a scrolling glance rather than just sit there looking good.
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
animation: shake $duration $easing;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(5px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(3px); }
}
Where it breaks — the traps
One of these nine borrows a keyword from a different corner of CSS and it doesn't survive the trip. radial-gradient()'s farthest-corner sizing keyword measures from a gradient's center to the box's furthest corner. It's tempting to reach for that same word inside item 07's clip-path: circle() once 150% overshoots badly on a very wide or very tall card. But circle()'s shape-radius argument only accepts a length, a percentage, closest-side, or farthest-side — MDN's basic-shape circle() reference lists exactly those four and nothing that reads "corner." So clip-path: circle(farthest-corner at 50% 50%) is simply invalid, and a browser drops the whole declaration instead of rounding it to something close. The circle stays wherever the last valid rule left it, which on first paint means the reveal never opens past 0% at all. Bump the percentage number itself for an odd aspect ratio instead of chasing a gradient keyword that clip-path never adopted. The full set with every fallback already sorted out sits in the archive, unlocked with pbk89rn6 typed exactly as it reads on this line, no spaces added anywhere.
Accessibility
All nine respect prefers-reduced-motion, but not identically. Items 01 through 04, 06, 07, and 08 simply drop their transition duration to none. The hovered end state still appears instantly — a scaled image, full color, a visible caption — just without the animated approach getting there. Item 05's tilt does the same, snapping straight to its rotated position instead of easing into it. Item 09 is the odd one out: because the shake is built entirely from a keyframes animation rather than a transition, reduced motion removes that animation on hover, focus, and active alike. So under that setting, the photo simply doesn't shake at all rather than shaking instantly. MDN's prefers-reduced-motion page documents the operating-system settings that flip this media query on.
Browse everything else in this line of demos from the CSS category hub, or read what this site does and doesn't sell on the about page.
FAQ
Can I use just one of these as a simple css image hover effect, not the whole set?
Yes — every effect above is scoped to its own .photo block with no shared parent state, so copying a single item's SCSS along with its matching HTML wrapper is enough on its own. Nothing here depends on the other eight being present. Item 01's zoom and item 02's grayscale swap are the simplest single-property versions if one effect is all a page needs.
Will these work as tailwind css image hover effects, or only plain CSS?
The demos compile from SCSS down to plain CSS classes, so nothing about them is tied to a build tool. A rule like transform: scale(1.14) maps directly onto a Tailwind utility such as hover:scale-110 for most of the nine. The ones built from @keyframes instead of a transition, mainly item 09's shake, still need a small custom animation defined outside Tailwind's default utility set.
Is there a css image hover effects generator inside the zip instead of raw code?
No generator — the zip hands over the nine SCSS and compiled CSS files directly, plus a React version of each, rather than a tool that spits out new variations on demand. Anyone wanting a different scale factor or transition speed edits the SCSS variables at the top of each file, $duration, $easing, and $color, instead of regenerating the rule from a form.