How to Build a Basic Card Hover Effect in CSS
The transition goes on the card’s resting rule. The hover rule changes transform and box-shadow, and it sits inside a hover media query.
The :hover pseudo-class matches while a pointer sits over the card or anything inside it. Hovering the image, or just the padding, counts as hovering the card.
If the card markup itself is still unsettled, a guide to building cards in CSS covers structure and layout first, and a roundup of CSS hover effects covers hover beyond cards.
- Wrap the content in one
.card container holding the image, heading, text and a single link. - Style the resting state with
border-radius, a soft box-shadow and overflow: hidden when the card carries an image. - Declare the transition on that same rule, listing
transform and box-shadow with a duration such as 200ms ease-out. - Write the hover rule inside
@media (hover: hover), applying translateY(-4px) and a deeper shadow to .card:hover. - Add
.card:focus-within to the same selector so a focused link inside the card triggers the same change.
Put together, those steps come out as two rules:
.card {
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: transform 200ms ease-out, box-shadow 200ms ease-out;
}
@media (hover: hover) {
.card:hover,
.card:focus-within {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
}
}
The transition belongs on the resting rule. Declared only inside :hover, it animates the card on the way in and snaps it back instantly on the way out.
Without a duration nothing moves, because the initial transition-duration is 0s (MDN Web Docs, 2026).
The hover media query keeps the lifted state off touch screens, where a tap can leave a hover state stuck on the card.
Common CSS Card Hover Effects, From Lift to 3D Flip
Almost all of them animate only transform or opacity. Shadow growth is the exception, and any colored glow built on box-shadow shares its cost.
Lift and Shadow
Lift moves the card up with transform: translateY(-4px), and a deeper box-shadow sells the movement. The shadow grows in offset and blur at the same time, so the card looks further from the page.
Travel of 4px to 8px suits compact cards. Go longer and the card starts to look like it’s leaving the grid. For a colored glow, swap the neutral shadow for something like 0 0 20px in the brand color.
Layered shadows read softer than one heavy shadow, and a rundown of shadow effects in CSS shows several ways to stack them.
Scale and Image Zoom
Scale makes the whole card bigger. Zoom works on the image alone, which is usually what you actually want.
Use transform: scale(1.03) on a card and a stronger value such as scale(1.08) on the image, with overflow: hidden on the image wrapper so the zoom stays inside the card edges.
Zoom is the safer pick for tight grids. A scaled card pushes its edges over the neighbors, so give the hovered card a higher z-index when the neighbors are positioned too.
Overlay and Text Reveal
An overlay is an absolutely positioned child at opacity: 0 that moves to opacity: 1 on hover. A caption slide works a little differently. The text starts at translateY(100%) inside an overflow: hidden container and slides to translateY(0).
Set pointer-events: none on the overlay so clicks reach the link underneath. Stack the layers with z-index so the heading stays readable above the dim layer.
Anything revealed only on hover is invisible to keyboard and touch users, so the same reveal has to fire on focus too.
3D Flip and Tilt
A flip card starts with perspective on the parent and transform-style: preserve-3d on the inner wrapper. Both faces get backface-visibility: hidden. On hover the wrapper rotates with rotateY(180deg), and the back face is pre-rotated by the same angle.
A step-by-step flip card in CSS shows the markup for both faces.
Pure CSS tilt stops at a fixed angle such as rotateX(4deg). Tilting toward the cursor needs JavaScript to read pointer position.
Flip hides the back face from anyone who never hovers, so keep key information on the front.
Dimming Sibling Cards With :has()
The :has() selector reads the hover state of a child to style its siblings:
.grid:has(.card:hover) .card:not(:hover) {
opacity: 0.6;
}
The grid container (CSS Grid or flexbox) notices a hovered card and dims every card that is not hovered. Dimming runs on opacity, so it stays on the compositor.
Baseline defines “widely available” as 30 months after a feature reaches all core browsers, which is exactly the gap between :has() becoming newly available in December 2023 (MDN Web Docs) and widely available in June 2026 (web.dev).
A browser without :has() drops the entire rule, so the cards simply stay at full opacity (MDN Web Docs).
What Transition Duration and Easing Suit Card Hover Effects?
Around 200ms with ease-out suits most card hovers. Treat it as a starting value rather than a standard, because lift distance and card size both shift the right number.
Nielsen Norman Group’s response time limits mark the outer bounds. A reaction under 0.1 second feels instantaneous, and 1.0 second is about the limit for keeping the user’s flow of thought uninterrupted (Nielsen Norman Group, 1993).
A card that takes 800ms to lift sits under that second and still feels sluggish. Hover feedback should register without drawing attention to itself.
For reference, ease is cubic-bezier(0.25, 0.1, 0.25, 1), the initial timing function of transitions (MDN Web Docs, 2026). ease-out is cubic-bezier(0, 0, 0.58, 1), a fast start and a slow finish (MDN Web Docs, 2026).
For hover, ease-out reads better than the default because the card responds fast and settles gently. linear looks mechanical on moving cards, so keep it for opacity fades.
A custom cubic-bezier() takes two control points with x values held between 0 and 1. The y values are unrestricted, which allows a slight overshoot (MDN Web Docs).
The destination state decides which transition runs. A transition declared in the :hover rule governs the way in, and the one on the resting rule governs the way out.
That split allows a quick 150ms entry and a slower 250ms return, which tends to feel natural on cards.
A card hover is about the smallest micro-interaction an interface has. It’s brief, and it belongs to a single action.
A transition generator for CSS is handy for comparing durations and curves before they go into the stylesheet.
Performance of Card Hover Effects: What to Animate and What to Avoid
Animate transform and opacity, and treat every other property as a cost to justify. Web.dev’s animation guide tells authors to restrict animations to those two so they stay on the compositing stage of the rendering path.
Browsers typically run CSS animations that touch only those properties on the compositor thread, so the motion keeps going while the main thread is busy (web.dev).
Transform (translateY, scale, rotate) and opacity are the cheap ones. Everything else needs checking. Box-shadow, border-color and background-color are the usual suspects, while width, height, margin, top and left are best avoided because they move the neighbors.
Box-shadow is the property cards animate most often from outside the cheap list. Chrome DevTools shows which areas repaint, so check it there instead of guessing.
The workaround is a pre-rendered shadow. Paint the final shadow on a ::after layer behind the card content, start it at opacity: 0, and fade it in on hover. The browser then animates opacity only.
Work out the target shadow in a box shadow generator for CSS first, then paste the values into the pseudo-element.
MDN treats will-change as a last resort for existing performance problems and advises against adding it to pages that already run well. Chrome reacts to some values by creating a compositing layer or a new stacking context.
That side effect matters on grids, because will-change: opacity creates the stacking context up front and can change which card paints on top.
In most cases MDN recommends applying the hint from a script around the interaction instead of leaving it in the stylesheet.
Record a Performance trace in Chrome DevTools while moving the pointer across the grid. Dropped frames point to the property that needs replacing.
Large blurred shadows across a dense grid are where box-shadow transitions tend to drop frames first.
Choosing a Hover Effect by Card Type
Link-only cards need little more than a lift with a one-step shadow increase. On image-led cards, zoom the image and leave the frame still. Text-dense cards get a border color or shadow change and nothing else, so reading is never interrupted.
Product cards can take a lift too. A quick-add button that appears only on hover is a bad idea.
Flip and reveal effects earn their place only when the extra content is optional. The flip card gets built most and delivers least, because it hides half the content behind a hover.
Price and the buy action belong in the resting state, which is where product card design conventions put them.
| Effect | Animates | Best for | Reduced motion and touch |
|---|
| Lift and shadow | transform, box-shadow | Link-only, blog and profile cards | Drop the lift, keep the shadow change |
| Image zoom | transform on the image | Image-led cards, galleries | Drop the zoom, change the border instead |
| Overlay reveal | opacity | Image cards with short captions | Fade stays; show the caption at rest on touch |
| 3D flip | transform | Cards with an optional back face | Swap the flip for a fade |
| Sibling dimming | opacity | Grids of equal cards | No movement, so it stays; irrelevant on touch |
Material’s web components limit elevation to levels 0 to 5 and animate shadows from one level to another with an ordinary transition (material-web.dev).
Borrow the idea and raise a card by one level on hover instead of inventing a new shadow for every card type.
A hover effect should support the card’s place in the page’s visual hierarchy, not compete with it. On a grid of equal cards, every card moves the same distance.
How Do You Make Card Hover Effects Accessible?
Every hover rule needs a keyboard focus twin, and movement should switch off for users who ask for reduced motion. Hover also shouldn’t be the only route to any information on the card.
Together these cover most of the places where hover effects touch web accessibility, and each one has its own trigger and its own rule.
Keyboard Focus Parity
W3C’s Understanding document for SC 1.4.13 states that content triggered by pointer hover should also be triggerable by keyboard focus.
The :focus-within selector matches a card whenever the card or a descendant holds focus, however focus arrived (MDN Web Docs). :focus-visible matches only when the browser decides focus should be evident, and W3C’s technique C45 notes that browsers always show it for keyboard users but generally not after a mouse click.
A keyboard rule that reacts only to keyboard focus looks like this:
.card:has(a:focus-visible) {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
}
.card a:focus-visible {
outline: 3px solid currentColor;
outline-offset: 4px;
}
The rule repeats the hover transform and shadow, and the link gets its own visible outline. Don’t strip that outline with outline: none unless a replacement indicator is in place.
Reduced Motion
MDN lists scaling or panning large objects among the motion types that can trigger vestibular discomfort. Card zoom and card flip cover large areas, so they sit closest to that list.
Opacity fades, color shifts, border changes and shadow changes can stay. The translateY lift, scale, image zoom, 3D flip and tilt should go.
@media (prefers-reduced-motion: reduce) {
.card:hover,
.card:focus-within {
transform: none;
}
.card:has(a:focus-visible) {
transform: none;
}
}
The override works because it has the same specificity as the hover rule and comes later in the source (MDN Web Docs).
WCAG splits the issue across levels. SC 2.3.3, Animation from Interactions, is Level AAA and asks that interaction-triggered motion can be disabled unless the motion is needed to convey information (W3C).
AA conformance does not require it, but the rule costs a few lines of CSS.
Content Revealed on Hover
Hide extras, not the main message.
SC 1.4.13, Content on Hover or Focus, is Level AA and applies to anything a card reveals on hover or focus (W3C Understanding document, revised July 2026). Escape has to close the reveal without moving the pointer or focus, unless the reveal covers nothing. An overlay inside the card stays hovered while the pointer crosses it. Persistence is the last condition, and it means the reveal stays until hover or focus leaves, the user dismisses it, or the information is no longer valid.
An overlay that covers the card’s own image or text falls under the dismissible condition, so it needs a small script for the Escape key.
W3C adds that more predictable ways of adding content usually exist and recommends them, which is why a caption shown at rest beats a caption revealed on hover. The same reasoning runs through accessible UI components in general: visible by default, enhanced on hover.
Why Does Card Hover Stick on Touch Screens?
Touch browsers emulate hover on tap, and the emulated state stays on the tapped element until the user taps somewhere else (Stefan Judis; MDN Web Docs).
Browsers built it that way on purpose, to give touch users a way to reach hover-only content.
The hover media feature reports whether the primary input can hover conveniently, and any-hover reports whether any available input can. Both have been available across browsers since December 2018 (MDN Web Docs).
Cards should key off the primary input. On a tablet with a trackpad attached, any-hover reports true even while a finger does the tapping, which brings the sticky state back.
.card .caption {
opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
.card .caption {
opacity: 0;
transition: opacity 200ms ease-out;
}
.card:hover .caption {
opacity: 1;
}
.card:has(a:focus-visible) .caption {
opacity: 1;
}
}
The caption is visible by default and hidden only where hover exists, which applies the logic of mobile-first design to a single effect.
Detection isn’t perfect either. Some Android browsers can match (hover: hover) on a touchscreen. Michelle Barker traces it to a long-press hover emulation on some Android versions, and ctrl.blog reports that Chromium on Samsung devices identifies touchscreens as touchpads.
The reverse happens as well. A WebKit bug report documented hover and pointer: fine failing to evaluate true when the mouse support added in iOS 13.4 was in use.
Pairing the hover query with (pointer: fine) filters the long-press case, as Michelle Barker documents. Neither failure breaks a card whose hover effect is decoration only.
Don’t detect touch devices, as Judis advises. Design for touch, mouse, stylus and keyboard together.
When Card Hover Effects Fail or Need a Fallback
A hover effect falls apart when it’s the only signal on a card, and again whenever the input can’t hover or the browser doesn’t know the selector behind it.
Failure Cases
- Hover as the only sign that a card is clickable, so the card looks static at rest
- A lift on the whole card when only the title is a link, which promises a click target the card does not deliver
- Flip cards whose back face holds more text than the front, because both faces share one height
- A quick-view panel beside the card that closes as the pointer crosses the gap, which W3C lists as failure F95 of SC 1.4.13
Each failure has the same cure. Build the card to work without the effect, then add the effect on top.
Browser Support Gaps
The four features behind keyboard, reduced-motion and touch handling on a card all carry Baseline status.
| Feature | Role on a card | Baseline status |
|---|
| :focus-within | Styles the card when a child holds focus | Widely available since July 2022 |
| :focus-visible | Styles keyboard focus only | Widely available since September 2024 |
| hover media query | Limits hover rules to hover-capable input | Widely available since June 2021 |
| prefers-reduced-motion | Removes movement on request | Widely available since July 2022 |
Status per MDN Web Docs and the web-features Baseline data. A fallback now matters mainly for older browsers that still sit in a project’s support list.
The @supports not selector() test from MDN’s @supports reference wraps a :focus fallback in a feature query:
@supports not selector(:focus-visible) {
.card a:focus {
outline: 3px solid #1a5fb4;
outline-offset: 4px;
}
}
Keep selectors built on newer pseudo-classes in their own rules, so one unsupported selector cannot discard the rest of a selector list.
Testing those older engines belongs to a wider cross-browser compatibility routine.
CSS Card Hover Effects FAQ
What is the difference between a hover transition and a keyframe animation on a card?
Transitions need a trigger such as :hover and play between two states, in both directions. Keyframe animations follow steps defined in @keyframes and can start on their own, which suits looping motion better than hover feedback.
Can you build card hover effects without JavaScript?
Yes. Lift, shadow growth, zoom, overlay reveal, flip and sibling dimming all run on CSS alone. JavaScript is needed only for cursor-tracking tilt or spotlights and for an Escape handler that dismisses a revealed overlay.
How do you make an entire card clickable without nesting links?
Put one link on the card heading, make the card position: relative, and stretch the link’s ::after pseudo-element over the card with position: absolute and inset: 0. The pseudo-element sits inside the card, so the hover rule keeps firing.
How do you add hover effects to a Tailwind CSS card?
Add transition, hover:-translate-y-1 and hover:shadow-lg to the card’s classes. Tailwind CSS v4 applies hover: utilities only on devices that support hover (Tailwind Labs), and the motion-safe: prefix drops the movement under reduced motion. A set of Tailwind card layouts supplies the markup.
How do you add a hover effect to a Bootstrap card?
Start from standard Bootstrap cards and write a custom rule on .card:hover for the transform and shadow. Then add .stretched-link to the card’s link. Bootstrap cards already carry position: relative, so no extra markup is needed (Bootstrap documentation).
How do you force a card’s hover state in Chrome DevTools?
Select the card in the Elements panel, click the :hov button in the Styles pane, and tick :hover under Force element state. The hover rule stays applied while the pointer sits over DevTools, and the same panel forces :focus.
Testing CSS Card Hover Effects Before Shipping
Hover with a mouse first, since that’s the check everybody already does. Then tab through the card grid, switch on reduced motion, and tap on a real phone. That order runs from cheapest to most expensive.
Keyboard and reduced-motion failures stay invisible to a developer using a mouse, yet both take minutes to check. Touch comes last because it needs hardware.
DevTools forces the :hover state only and leaves the hover media query untouched. The Samsung false positive and the iOS 13.4 false negative therefore show up only on physical devices.
The cost is a plainer card for reduced-motion and touch users, who lose the lift and the reveal. Verified in October 2026, this advice holds until browsers correct hover reporting on hybrid devices.