20 Tailwind CSS image hover effects for portfolio grids, e-commerce product cards, team-member galleries, and full-bleed hero navigation — contained zoom and grayscale-to-color filters, sliding captions and text reveals, product image swaps, glassmorphism and neumorphic panels, neon and glare sweeps, clip-path and split transitions, pointer-tracked 3D tilt, and card flips. Scoped under .tih-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 20 Tailwind image hover effects. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
The classic contained zoom, ported to one variant: overflow-hidden on the card and group-hover:scale-112 on the image, so the scale is clipped at the card boundary and the grid never shifts. The hover state lives on the element that moves — not in a .card:hover .img rule several screens away.
The portfolio-grid reveal: a gradient overlay fades in and its three text lines slide up in sequence. The CSS version staggers with duration multipliers; the port staggers with the per-item index pattern — each line carries style="--i:1" and one shared utility reads it, so the cascade tempo is a single number.
The shopping-grid cross-dissolve: two stacked layers, one fading out as the other fades in, with a half-percent scale difference that keeps it from reading as a flat dip. Both layers state their resting and hovered geometry on themselves, so the pair can never drift out of sync — which is the entire failure mode of this pattern.
Blur the image through the overlay rather than blurring the image: group-hover:backdrop-blur-lg plus group-hover:backdrop-brightness-60, which is the port's cleanest single win — Tailwind ships composable backdrop-filter utilities, so a two-function filter stack that was one hand-written declaration is now two named utilities that read like the intent.
A caption panel parked below the frame with a 44px strip still showing, so the card advertises itself before anyone hovers. The resting offset is [translate:0_calc(100%-44px)] — an arbitrary translate, because the value is tied to the peek height, not to the spacing scale.
Three filter functions transitioned as one unit — and in utilities they are three named classes: grayscale brightness-75 saturate-0 at rest, group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 on hover. The 20% saturation overshoot is what makes the colour land rather than merely appear.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A brand colour washed over a photograph with mix-blend-color, which replaces hue and saturation while keeping luminosity — a lighting gel, not a paint bucket. The tint is a per-card inline custom property, so the overlay is one class list serving any colour, and the text sits in an isolate wrapper so the blend cannot touch it.
Soft UI is two shadows going in opposite directions, and the port's win is that they become tokens: --shadow-tih08-out and --shadow-tih08-in in @theme mint shadow-tih08-out and shadow-tih08-in, so the raised and pressed states are named rather than re-typed as eight-value strings on every card and button.
A frosted panel parked below the frame that rises into the card on hover. Two backdrop utilities do the glass (backdrop-blur-xl backdrop-saturate-150), one arbitrary translate parks it, and the resting title fades out as the panel arrives — so the card never shows two competing labels mid-transition.
Three stacked shadows in one property — hairline, 20px bloom, 60px ambient — driven by a per-card --glow custom property, so one class list glows in three colours. The corner brackets, which the CSS version builds from two pseudo-elements, become before: and after: utilities on the frame.
Shape morphing where both shapes sit on the same element as two arbitrary clip-path utilities — resting and hovered, one line apart. Since interpolation requires matching vertex counts, having both values visible together is not cosmetic: it is what stops the pair from silently degrading into a hard cut.
The pointer-tracked tilt, with the split the port makes possible: JavaScript writes four numbers into custom properties, and every piece of geometry stays in the markup. [transform:perspective(800px)_rotateX(var(--rx))_rotateY(var(--ry))_scale3d(var(--s),var(--s),var(--s))] is one utility reading live values — the script never touches a class or a style string.
A bright band swept across the artwork by a single pseudo-element — and in utilities the entire sweep is an after: chain on the frame: after:[translate:-200%] group-hover:after:[translate:300%]. No extra markup, no separate rule block, and the two ends of the travel sit next to each other.
The card slices in half and an action strip wipes open in the gap. Two layers carry identical artwork and complementary clip-path polygons; the strip animates inset(100%_0_100%_0) to inset(0), so it opens from the centre line outward rather than fading in.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Three concentric rings pulse outward on hover. The port makes two changes on the way: the keyframe becomes an --animate-tih15-ripple token (so group-hover:animate-tih15-ripple is a real utility), and the rings expand with scale instead of width/height — the fix the CSS version lists as a customization, taken as the default.
A comet-tail arc that spins only while hovered — not by adding the animation, but by flipping animation-play-state from paused to running, so the arc resumes from wherever it stopped. In utilities that is two arbitrary properties on a before: chain, and the rest of the ring is bg-conic-[…].
A nav where hovering an item swaps the whole viewport background — and the port replaces six class toggles with one attribute. The stage carries data-active="3"; each panel reads it through group-data-[active=3]:opacity-100. The script sets one value and never touches a class list.
Column-based masonry where the responsive column count is the whole layout: columns-2 sm:columns-3 plus break-inside-avoid on each item. The CSS version needs a media query to change column count; here it is one breakpoint prefix, which is the kind of edit this port makes trivial.
The artwork darkens and an action button pops in at the centre, spinning up from scale-50 -rotate-20 to rest. In v4 those are two individual properties, which quietly removes the CSS version's biggest footgun: transform function order no longer matters, because there is no shorthand to order.
The three-layer flip — scene, flipper, two faces — with every one of its four critical properties now a named utility: [perspective:800px], transform-3d, backface-hidden and group-hover:rotate-y-180. The pattern's failure modes are all “you forgot one of these”, and a class list makes a missing one visible.
FAQ
Frequently asked questions
How does the contained zoom in Demo 01 keep the enlarged image inside the card frame?
The trick is two elements, not one. The card is the frame and owns overflow-hidden; the image inside scales past 100% on hover but everything spilling past the card edge is clipped, so the frame never moves. Wire it with the group pattern: mark the card group, then scale the image with transition-transform duration-500 group-hover:scale-110. If you forget overflow-hidden, the whole picture visibly grows and shoves the surrounding grid on every hover. Add will-change-transform only if you see repaint jank — scale is compositor-friendly on its own, so you rarely need it. Keep the transition on the image, not the card, or the frame animates too.
What is the group and group-hover mechanism these demos rely on?
Almost every effect here is one parent reacting to hover and many children responding. In utility CSS you cannot write a descendant selector inline, so Tailwind marks the parent group and each child opts in with a group-hover: prefix. The overlay in Demo 02 fades with group-hover:opacity-100, the three caption lines each slide up with their own group-hover:translate-y-0, and Demo 03 cross-dissolves two stacked layers by pairing group-hover:opacity-0 on one with group-hover:opacity-100 on the other. The parent needs no hover styles of its own — it is only a scope marker. The common mistake is putting group-hover: on the same element you forgot to nest under something tagged group; the class then silently does nothing because there is no ancestor group to trigger it.
Why does my zoom or reveal effect flicker or jump on the first hover?
Two usual causes. First, you animated a property that triggers layout — animate transform and opacity, never width, height, or top. Demo 01 scales, Demo 05 slides its caption with translate-y, and both stay on the compositor. Second, you put the transition class only inside the hover variant. The transition must live on the base element so it applies in both directions; otherwise the enter is instant and only the leave eases, which reads as a jump. Write transition-transform duration-300 on the element and let group-hover:scale-105 supply the target. For the stacked-layer swaps in Demo 03 and Demo 14, give both layers the same transition and offset one by a fraction of the duration so the cross-fade has no dead frame where the card looks empty.
How do I respect prefers-reduced-motion with these Tailwind hover effects?
Tailwind ships a motion-reduce: variant that maps to the prefers-reduced-motion: reduce media query, so you disable motion per element without leaving your markup. On the zoom and tilt demos add motion-reduce:transform-none motion-reduce:transition-none so the image holds still for users who asked for less movement. The important nuance: a reveal that only exists on hover, like the caption in Demo 02, must still expose its text when motion is reduced — don't just freeze it in the hidden state. Either keep the overlay readable at rest for those users or ensure the same information lives in the DOM regardless. Continuous loops like the Demo 15 ripple and the Demo 16 comet arc should stop entirely under motion-reduce, since a permanent animation is exactly what the setting exists to silence.
Are hover-only reveals like the caption and icon demos accessible to keyboard users?
Hover alone is not reachable by keyboard or touch, so any information that only appears on :hover is invisible to a large group of users. If the card is interactive — the icon button in Demo 19, the action strip in Demo 14, the nav items in Demo 17 — make the trigger a real focusable element and mirror every group-hover: with a matching group-focus-within: so tabbing in reveals the same overlay. Give the wrapping link or button a visible focus-visible:ring too. If the revealed content is purely decorative flavour text, hover-only is acceptable. But never hide essential copy — a price, a product name, a link label — behind hover alone. Put that text in the normal document flow and let the hover effect enhance it, not gate it.
How does the 3D tilt in Demo 12 pass pointer position into Tailwind classes?
You cannot compute a live pointer angle in a utility class, so the port splits the work. JavaScript does the only thing that must be dynamic: it reads the pointer position over the card and writes four numbers into CSS custom properties — the rotate-X, rotate-Y, and the glare offsets. The markup then consumes those variables through arbitrary-value utilities such as [transform:perspective(800px)_rotateX(var(--rx))_rotateY(var(--ry))], and transition-transform smooths the settle when the pointer leaves. The listener stays tiny — no class toggling per frame, which would thrash the DOM. One gotcha: set transform-style: preserve-3d and a perspective on the parent, or the rotation looks flat. Reset the variables to zero on pointer-leave so the card eases back rather than sticking at its last angle.
The grayscale-to-color and blur hover effects feel sluggish on large images. Why?
Filters are not free the way transform is. Demo 06 transitions grayscale, contrast, and brightness as a group, and Demo 04 uses backdrop-blur through the overlay — both force the GPU to re-sample the image every frame, and blur especially scales with pixel count. Keep the source image close to its displayed size instead of shipping a 4000px photo into a 400px card; that single change usually removes the lag. Prefer backdrop-blur over blurring the image element directly when you want text to stay sharp, as Demo 04 does. Cap the transition at duration-300 or so — a long filter transition just extends the expensive part. If a whole grid animates filters at once on hover, stagger nothing; only the hovered card is filtering, so the cost is per-card, not per-grid.
When should I not use these image hover effects at all?
Hover is a pointer affordance, so on touch screens none of these fire — a phone user taps and gets either nothing or a stuck sticky-hover state. If the effect carries meaning rather than decoration, it is the wrong pattern for a touch-first layout; put the caption, price, or action in the always-visible flow and treat the hover as a desktop bonus. Skip the heavy ones — the Demo 20 3D flip, the Demo 12 tilt, the Demo 04 backdrop-blur — on dense grids of many cards where several could animate at once, or on low-powered devices. And don't stack three effects on one card; a zoom plus a color shift plus a rising caption competes for attention and slows the interaction. Pick the single effect that matches the content: product swaps for shops, captions for portfolios, subtle zoom for editorial.