Guides/ CSS
touch_app

15 CSS Hover Effects with Code Examples

A good hover effect does three things: tells the user the element is interactive, rewards them for paying attention, and stays out of the way on mobile. This collection covers 15 modern, accessible, copy-and-paste patterns for buttons, cards, images, and icons — all with pure CSS, no JavaScript.

April 2026 · 10 min read

Principles Before the Code

Three rules to keep any hover effect feeling professional rather than gimmicky:

  • Always animate via transition, never by swapping properties instantly. A 150–250ms transition feels intentional; 0ms feels broken.
  • Use transform and opacity for movement — they're GPU-accelerated. Avoid animating width, height, margin, or top, which trigger layout recalcs and jank.
  • Respect prefers-reduced-motion — users who disabled motion system-wide don't want your animations.
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

1. Lift on Hover (Card)

The universal "this is clickable" card pattern. Works for blog cards, tool cards, product grids.

.card {
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 32px -8px rgba(0, 0, 0, 0.25);
}

2. Button Fill Slide

A background fills from left to right. Uses a pseudo-element to avoid messing with the button's actual background.

.btn-fill {
  position: relative;
  overflow: hidden;
  color: #7c5cfc;
  border: 2px solid #7c5cfc;
  padding: 0.75rem 1.5rem;
  transition: color 0.3s ease;
  z-index: 0;
}
.btn-fill::before {
  content: '';
  position: absolute;
  inset: 0;
  background: #7c5cfc;
  transform: translateX(-100%);
  transition: transform 0.3s ease;
  z-index: -1;
}
.btn-fill:hover { color: #fff; }
.btn-fill:hover::before { transform: translateX(0); }

3. Underline Slide (Link)

An underline that grows from the left instead of appearing instantly. The most popular modern link hover.

.link-underline {
  position: relative;
  text-decoration: none;
  color: #fff;
}
.link-underline::after {
  content: '';
  position: absolute;
  left: 0; bottom: -2px;
  width: 100%; height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s ease;
}
.link-underline:hover::after { transform: scaleX(1); }

4. Image Zoom (Inside Container)

The image scales but the container doesn't — the zoom is clipped by overflow: hidden.

.img-zoom {
  overflow: hidden;
  border-radius: 12px;
}
.img-zoom img {
  transition: transform 0.5s ease;
}
.img-zoom:hover img {
  transform: scale(1.08);
}

5. Glow Outline (Accessible Focus Ring)

A soft glow that works for both :hover and :focus-visible — doubles as an accessibility feature.

.btn-glow {
  transition: box-shadow 0.25s ease;
}
.btn-glow:hover,
.btn-glow:focus-visible {
  box-shadow: 0 0 0 4px rgba(124, 92, 252, 0.3);
  outline: none;
}

6. Icon Rotate

A gear, refresh, or menu icon that rotates on hover — a small detail that rewards interaction.

.icon-rotate {
  display: inline-block;
  transition: transform 0.4s ease;
}
.icon-rotate:hover {
  transform: rotate(90deg);
}

7. Arrow Slide Forward

The classic "Read more →" effect. Arrow nudges right when you hover its parent link.

.link-arrow .arrow {
  display: inline-block;
  transition: transform 0.2s ease;
}
.link-arrow:hover .arrow {
  transform: translateX(4px);
}

8. Gradient Shift

A button where the gradient shifts position on hover. The trick is a 200% background size moved between positions.

.btn-gradient {
  background: linear-gradient(135deg, #7c5cfc, #ec4899, #7c5cfc);
  background-size: 200% 200%;
  background-position: 0% 50%;
  transition: background-position 0.5s ease;
  color: #fff;
  padding: 0.75rem 1.5rem;
  border-radius: 8px;
}
.btn-gradient:hover {
  background-position: 100% 50%;
}

9. Tilt (3D Perspective)

A subtle 3D tilt that suggests depth. Use sparingly — too much tilt feels gimmicky.

.tilt {
  transform-style: preserve-3d;
  perspective: 1000px;
  transition: transform 0.3s ease;
}
.tilt:hover {
  transform: perspective(1000px) rotateX(4deg) rotateY(-4deg) translateZ(10px);
}

10. Shine Sweep (Premium)

A thin diagonal shine sweeps across the element. Great for CTA buttons, badges, or premium labels.

.shine {
  position: relative;
  overflow: hidden;
}
.shine::after {
  content: '';
  position: absolute;
  top: 0; left: -100%;
  width: 50%; height: 100%;
  background: linear-gradient(90deg,
    transparent,
    rgba(255, 255, 255, 0.2),
    transparent);
  transform: skewX(-20deg);
  transition: left 0.6s ease;
}
.shine:hover::after {
  left: 150%;
}

11. Border Draw

A border draws itself around the element. Done with two pseudo-elements that animate from opposite corners.

.border-draw {
  position: relative;
  padding: 1rem 2rem;
}
.border-draw::before,
.border-draw::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 2px solid transparent;
  transition: border-color 0.3s, width 0.3s, height 0.3s;
}
.border-draw::before {
  width: 0; height: 100%;
  border-top-color: #7c5cfc;
  border-left-color: #7c5cfc;
}
.border-draw:hover::before {
  width: 100%;
  transition: width 0.3s ease, height 0.3s ease 0.3s;
}

12. Scale & Shadow Combo

Subtle scale with a deeper shadow — feels tactile. Good for photo grids.

.scale-shadow {
  transition: transform 0.3s, box-shadow 0.3s;
}
.scale-shadow:hover {
  transform: scale(1.03);
  box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.3);
}

13. Caption Slide-Up (Image Overlay)

A caption slides up from the bottom of an image — perfect for portfolios and galleries.

.caption-slide {
  position: relative;
  overflow: hidden;
}
.caption-slide .caption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.75);
  color: #fff;
  transform: translateY(100%);
  transition: transform 0.3s ease;
}
.caption-slide:hover .caption {
  transform: translateY(0);
}

14. Color Desaturate on Siblings

In a grid, fade siblings when one is hovered — directs focus elegantly. Uses :has(), supported in all evergreen browsers since 2023.

.grid .item {
  transition: opacity 0.3s, filter 0.3s;
}
.grid:has(.item:hover) .item:not(:hover) {
  opacity: 0.5;
  filter: saturate(0.5);
}

15. Ripple Expand (Material-Style)

A circular pulse that expands from the center. Pure CSS approximation of Material Design's ripple.

.ripple {
  position: relative;
  overflow: hidden;
}
.ripple::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 0; height: 0;
  border-radius: 50%;
  background: rgba(124, 92, 252, 0.3);
  transform: translate(-50%, -50%);
  transition: width 0.5s ease, height 0.5s ease;
}
.ripple:hover::after {
  width: 300%;
  height: 300%;
}

Frequently Asked Questions

Do hover effects work on mobile?
Most don't — mobile has no hover state. Tapping triggers :hover briefly before navigating, which often causes visual glitches. Use @media (hover: hover) to scope hover effects to devices that support them, and add matching :active states for mobile feedback.
What's the ideal transition duration?
150–300ms for most hovers. Under 100ms feels instant (no perceived animation). Over 400ms feels sluggish — by the time the animation finishes, the user has already moved on. For decorative effects like shines or ripples, 400–600ms is acceptable.
Should I use ease-in-out or a custom curve?
cubic-bezier(0.4, 0, 0.2, 1) (Material's standard ease) is the safest default — it starts fast and ends soft. ease-out works for most cases. Avoid ease-in for hovers — it feels like the animation is starting late.
How do I hover on child elements when the parent is hovered?
Use the descendant selector: .parent:hover .child { ... }. This applies when the mouse is anywhere over .parent, which includes .child. It's how examples 4, 7, and 13 above work.
Why does my hover effect flash or flicker?
Two common causes: (1) the hover is changing a property that affects the element's size (like border-width), which causes layout shift and the cursor briefly leaves the element; (2) you're missing a transition, so the change happens instantly when entering or leaving hover. Animate transforms instead of layout properties, and always set a transition.