GODRICH

9 CSS Card Hover Effects — One Line, Copy-Paste

These nine css card hover effects each act on a whole card component at once, not a single button inside it, using nothing heavier than transform, opacity, and

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below follows how a card actually gets used: 01 and 02 give a card presence before any pointer arrives, and 03 through 05 add depth or reveal extra content once one lands. 06 through 08 nudge toward the next click, and 09 confirms that click actually registered. Watch the grid above instead of trying to click it — each thumbnail plays inside a fixed iframe that a real pointer can never reach. So every card loops through its hover state automatically, through the same is-demo class the standalone demos below rely on. Every effect also mirrors its :hover state onto :focus-visible and a matching @media (hover: none) block, so keyboard and touch visitors land on the same finished look a mouse would trigger. Two of the nine — 03 and 08 — additionally read the pointer's live position through a short JavaScript listener; the rest run on CSS alone once a card gets hovered, focused, or pressed.

01Lift with shadow

The whole card lifts 8px on hover, focus, and press alike while its box-shadow deepens from a pressed default into a raised one, sharing a single transition so neither property finishes ahead of the other. It suits a grid of product or article cards, where the raised card needs to stand out from its neighbors before anyone commits to a click.

transform: translateYbox-shadow 확대transition
.card {
  transform: translateY(0);
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.card:hover,
.card:focus-visible,
.card:active {
  transform: translateY(-8px);
  box-shadow: $shadow-raised;
}

02Spinning border light

A conic-gradient sliver of light starts spinning along the card's outline the moment hover — or the idle grid loop — fades its wrapper's opacity in, rather than turning underneath a hidden layer the whole time. The rotation itself comes from animating a registered @property angle rather than nudging a background position, so the light sweeps smoothly instead of jumping between fixed stops.

conic-gradient@property angleopacity
@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.card__ring {
  background: conic-gradient(from var(--angle), transparent 0deg, $color 40deg, transparent 100deg);
  opacity: 0;
}
.card:hover .card__ring,
.card:focus-visible .card__ring {
  opacity: 1;
  animation: spin $duration linear infinite;
}
@keyframes spin { to { --angle: 360deg; } }

033D tilt parallax

Eight lines of JavaScript turn the pointer's position inside the card into three CSS custom properties, and the card's rotateX/rotateY read those variables live. A background layer sits behind it on a fixed translateZ offset and shifts the opposite way through its own translateX value. Tabbing to the card or pressing it without a mouse falls back to one fixed tilt angle instead of tracking anything, so the sense of depth still shows up without a pointer in the room.

perspectiverotateX/rotateYJS 좌표
card.addEventListener('pointermove', function (e) {
  var r = card.getBoundingClientRect();
  var px = (e.clientX - r.left) / r.width - .5;
  var py = (e.clientY - r.top) / r.height - .5;
  card.style.setProperty('--rx', (-py * 14) + 'deg');
  card.style.setProperty('--ry', (px * 16) + 'deg');
  card.style.setProperty('--pz', (px * -12) + 'px');
});

04Content slides up

A caption panel sits pushed down to 70% of its own height under the card's media. Hover, focus, or the idle loop slides it fully into view while its meta line fades in over the same span. The card's own height never changes, since the panel is positioned absolutely rather than pushing the layout around it.

transform: translateYoverflow: hiddenopacity
.card__panel {
  transform: translateY(70%);
  transition: transform $duration $easing;
}
.card:hover .card__panel,
.card:focus-visible .card__panel {
  transform: translateY(0);
}

05Background zoom

The full card background scales from 1 to 1.12 on hover or focus, and overflow: hidden on both the card and the background layer keeps the zoomed edge from ever crossing the card's rounded corners. A sun shape and a hill shape sit on top of the gradient specifically so the zoom has hard edges to move against — the reason matters, and it's covered in the trap below.

transform: scaleoverflow: hiddenborder-radius 유지
.card { overflow: hidden; }
.card__bg {
  transform: scale(1);
  transition: transform $duration $easing;
}
.card:hover .card__bg,
.card:focus-visible .card__bg {
  transform: scale(1.12);
}

06Icon bounce

An arrow badge in the card's corner plays a two-cycle translateY bounce the instant hover, focus, or a press begins. It uses a springy cubic-bezier easing instead of a linear one, so each bounce feels elastic rather than mechanical. The grid preview runs a separate, endlessly looping version of the same keyframes so it has motion to show without a pointer ever landing on it.

@keyframes bouncecubic-beziertransform: translateY
.card__icon { transform: translateY(0); }
.card:hover .card__icon,
.card:focus-visible .card__icon,
.card:active .card__icon {
  animation: bounce $duration $easing 2;
}
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-8px); }
}

07Gradient border reveal

A plain 2px gray border sits on the card at rest. A second, absolutely positioned layer carrying a two-color linear-gradient fades in over it on hover, through two parallel mask declarations that punch out everything except a thin ring. Nothing about the card's box size or padding shifts, since that gradient layer only ever toggles opacity.

mask-compositelinear-gradientopacity
.card__border {
  background: linear-gradient(135deg, $color, $subject-cream);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity $duration $easing;
}
.card:hover .card__border,
.card:focus-visible .card__border {
  opacity: 1;
}

08Cursor spotlight

Ten lines of JavaScript write the pointer's position into two CSS custom properties on every pointermove, and a radial-gradient circle reads those variables live to trail the cursor across the card's dark surface. Because --mx, --my, and --sp are registered with @property, the browser interpolates the move itself instead of the light jumping between spots. Keyboard focus centers that same spotlight in the card's middle rather than chasing a pointer that was never there.

pointermoveCSS 변수radial-gradient
card.addEventListener('pointermove', function (e) {
  var r = card.getBoundingClientRect();
  card.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
  card.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%');
  card.style.setProperty('--sp', 1);
});

09Press feedback

The card only reacts to :active, never :hover, shrinking to scale(.97) and flattening its shadow back to the pressed variant for as long as a finger or mouse button stays down. Releasing reverses both properties on the same short transition, so the card visibly springs back the instant contact ends.

:activetransform: scalebox-shadow 축소
.card {
  transform: scale(1);
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.card:active {
  transform: scale(.97);
  box-shadow: $shadow-press;
}

Where it breaks — the trap

Item 07 is the one demo here built from two parallel declarations doing the same job for two different rendering engines. Keeping only one of them breaks the reveal without any warning in the console. The masked ring depends on -webkit-mask-composite: xor for WebKit's older compositing keywords and mask-composite: exclude for the standard CSS Masking property that replaced it. Those two properties don't share a vocabulary at all. MDN's mask-composite reference lists only add, subtract, intersect, and exclude as valid values for the unprefixed property. xor is not one of them, even though it reads like the natural modern spelling of the WebKit keyword. Write mask-composite: xor out of habit, and a browser reading the unprefixed property treats the whole declaration as invalid, drops it outright, and never rounds it to the nearest match. So the ring stays invisible in any engine that only understands the standard property. The unprefixed property itself is younger than the prefixed one it replaces — Chrome and Edge 120+, Safari 15.4+, Firefox 53+ per caniuse. So a project still supporting older Chrome builds needs the -webkit- line kept in place beside it, not swapped out for it. Item 07's own folder in the archive already carries both lines correctly paired, opened with 45c9tna8 keyed in exactly as it's spelled on this line, no spaces added, no letters capitalized.

Accessibility

Accessibility here mostly means the same finished look staying reachable without a pointer, but the nine split into two shapes under reduced motion. 01, 05, 07, and 09 simply drop their transition, so hover, focus, or a press still lands on the exact same lifted, zoomed, bordered, or pressed state — just without the animated trip getting there. 04 goes one step further and pins its caption panel open at all times, so the extra description never depends on hover firing at all. 02's spinning ring and 06's bounce disappear differently. 02 freezes at a fixed, dim ring rather than either hiding completely or keeping its full spin. 06's two-cycle bounce is removed outright, since its own keyframes already return the icon to its starting position by the end. 03's tilt and 08's spotlight are the two reading a live pointer position through JavaScript rather than a CSS transition. Reduced motion still strips their eased approach, but the values a mouse keeps writing into --rx, --ry, --mx, and --my keep updating in real time regardless. So a keyboard user gets one fixed fallback angle, while a mouse user's own movement is the one thing the media query never reaches. The setting itself is explained on MDN's prefers-reduced-motion page.

The rest of this card-hover set lives on the css hub, and what actually ships inside the download versus what only gets described here is covered on the about page.

FAQ

Can two of these css card hover effects run on the same card at once?

Yes — each effect only touches its own property or its own layer. 01 changes transform and box-shadow on the whole card, and 07 fades in a separately masked border layer, so neither competes with the other. The one pairing worth avoiding is two effects writing to the same custom property or the same transform on the same element, such as 03's tilt and 09's press. Whichever declaration comes second in the stylesheet simply overrides the first.

Do all nine of these css card hover effects need JavaScript to work?

No — only 03's tilt and 08's spotlight read a live pointer position, and both do it in eight to ten lines that write straight into CSS custom properties rather than touching layout. The other seven run entirely on :hover, :focus-visible, and :active, with no script involved at any point. Even 03 and 08 ship a CSS-only fallback: tabbing to the card without a mouse still lands on one fixed tilt angle or a centered spotlight. That fallback is written directly into the stylesheet rather than into the pointer listener.

Does animating box-shadow for a hover effect cost more than animating transform?

A little, yes — only 01 and 09 in this set touch box-shadow on hover or press. Unlike transform, changing a shadow forces the browser to repaint the area the shadow covers instead of just recompositing a layer. Scoped to one hovered card at a time, that repaint stays small enough not to show up as dropped frames. That's part of why the other seven effects lean on transform and opacity instead, once the motion needs to loop continuously rather than fire once per interaction.

Enter the archive password

The password is inside this article. You will find it as you read.