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
transformandopacityfor movement — they're GPU-accelerated. Avoid animatingwidth,height,margin, ortop, 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
:hoverbriefly before navigating, which often causes visual glitches. Use@media (hover: hover)to scope hover effects to devices that support them, and add matching:activestates 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-outworks for most cases. Avoidease-infor 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 atransition, so the change happens instantly when entering or leaving hover. Animate transforms instead of layout properties, and always set a transition.