CSS Hover Effects Gallery: 10 Patterns Beyond color and opacity
Ten battle-tested CSS hover patterns — clip-path reveals, magnetic pulls, neon glows, and more — with copy-paste code and no JavaScript required.
Why color and opacity are just the beginning
Most developers stop at color and opacity on hover and call it done. That works, technically. But you're leaving a ton of expressiveness on the table — the kind that makes someone actually *feel* a UI rather than just read it.
CSS has matured a lot since 2015. Properties like clip-path, filter, backdrop-filter, and transform are all GPU-composited in modern browsers, meaning they animate at 60fps without touching layout. That's the green zone you want to live in for hover effects.
Honestly, the patterns below aren't obscure tricks. They're just underused. Each one is production-ready and works in every browser that matters in 2026. No polyfills, no JavaScript, no drama. If you want live previews of these concepts in context, browse components — most of them already use these techniques under the hood.
One more thing — performance first. All these patterns animate transform, filter, clip-path, or opacity. None of them trigger layout recalculation. That's the rule: if your hover effect touches width, height, padding, or margin, you're painting yourself into a corner.
1 — Clip-Path Reveal (The Wipe)
This is probably the most dramatic single-property hover effect you can write. A clip-path transition sweeps a colored overlay across a card, revealing new content underneath. It's the hover equivalent of a cinematic wipe cut.
The trick is animating between two polygon() values that share the same vertex count. Browsers can't interpolate between inset() and polygon(), so pick one shape system and stick to it.
.card {
position: relative;
overflow: hidden;
}
.card::after {
content: '';
position: absolute;
inset: 0;
background: hsl(260 80% 55%);
clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
transition: clip-path 0.4s cubic-bezier(0.77, 0, 0.175, 1);
}
.card:hover::after {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}Worth noting: the cubic-bezier curve above is an ease-in-out that feels mechanical — like a real wipe. Swap it for ease and it gets mushy. Keep the 0.4s duration; below 0.3s it looks like a glitch, above 0.5s it feels sluggish on repeat interactions.
You can layer the text on top of the ::after pseudo-element using position: relative; z-index: 1 on the text wrapper. This keeps your HTML clean — one element, two states, zero JavaScript.
2 — Magnetic Pull with CSS Custom Properties
Pure magnetic pull (the button that follows your cursor) does need a tiny bit of JavaScript to track mouse coordinates. But 80% of the *feeling* — the spring back, the depth — comes entirely from CSS.
You pipe --x and --y custom properties from a JS mousemove listener, then CSS handles all the visual work. The separation is clean: JS measures, CSS renders.
.magnetic-btn {
--x: 0px;
--y: 0px;
display: inline-block;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
transform: translate(var(--x), var(--y));
}
.magnetic-btn:not(:hover) {
--x: 0px;
--y: 0px;
}// Minimal driver — swap for your own event delegation
document.querySelectorAll('.magnetic-btn').forEach(btn => {
btn.addEventListener('mousemove', e => {
const rect = btn.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = (e.clientX - cx) * 0.3; // 0.3 = pull strength
const dy = (e.clientY - cy) * 0.3;
btn.style.setProperty('--x', `${dx}px`);
btn.style.setProperty('--y', `${dy}px`);
});
btn.addEventListener('mouseleave', () => {
btn.style.setProperty('--x', '0px');
btn.style.setProperty('--y', '0px');
});
});The spring cubic-bezier (0.34, 1.56, 0.64, 1) is the key. It overshoots slightly on the return — that's what makes it feel physical. You can preview this kind of interactive effect in the Empire UI's cursor section; the custom cursor components use a similar spring math.
3 — Neon Glow Pulse and 4 — Inner Shadow Inset
Neon glow on hover is a staple of the cyberpunk aesthetic — and it's three lines of CSS. The secret is layering multiple box-shadow values at different spreads: a tight 0px blur for the hard edge, then a wider 20px spread for the atmospheric bleed.
.neon-btn {
border: 2px solid hsl(180 100% 60%);
color: hsl(180 100% 60%);
transition: box-shadow 0.25s ease, color 0.25s ease;
}
.neon-btn:hover {
box-shadow:
0 0 4px hsl(180 100% 60%),
0 0 20px hsl(180 100% 60% / 0.6),
0 0 60px hsl(180 100% 60% / 0.2),
inset 0 0 20px hsl(180 100% 60% / 0.1);
color: white;
}Notice the inset shadow in that last line. That's actually pattern #4 bundled in. The inset version creates depth — it makes the button look like it's glowing *from inside* rather than just being lit from outside. Combine them and you get the kind of button that people screenshot.
Quick aside: box-shadow is not GPU-composited the same way transform is, but it's still safe to animate in modern browsers. The compositing improved significantly in Chrome 112 and Firefox 115. In practice, you won't see jank unless you're stacking more than ~8 shadow layers on hundreds of elements simultaneously.
For the inset-only version as its own pattern — think neumorphism buttons — you'd animate from a protruding shadow to a recessed one. If you want pre-built neumorphism components, Empire UI's neumorphism hub has a full set with the shadow math already dialed in.
5 — Underline Draw and 6 — Border Trace
The animated underline draw is the workhorse of text links. You've seen it a thousand times — and yet most implementations are wrong. The common mistake is animating width from 0 to 100% on a ::after pseudo-element. That triggers layout. The correct way uses transform: scaleX().
.link {
position: relative;
text-decoration: none;
}
.link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: currentColor;
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
.link:hover::after {
transform: scaleX(1);
transform-origin: left;
}The transform-origin swap between rest and hover state is what makes it draw left-to-right on hover and erase right-to-left on unhover. That bidirectionality feels intentional. People notice.
Border trace (pattern #6) extends this idea to all four sides of a box. You need four pseudo-elements or SVG stroke-dashoffset animation. The SVG approach is cleaner for non-rectangular shapes. Using stroke-dasharray equal to the path length and animating stroke-dashoffset to 0 gives you a perfect traced border at any border-radius.
/* SVG border trace */
.traced-card svg rect {
stroke-dasharray: 800; /* match perimeter of your rect */
stroke-dashoffset: 800;
transition: stroke-dashoffset 0.6s ease;
}
.traced-card:hover svg rect {
stroke-dashoffset: 0;
}7 — Perspective Tilt and 8 — Skew Shear
Perspective tilt — the card that rotates in 3D as your cursor moves over it — is another effect that needs mousemove for the exact angles. But again, all rendering is CSS. You set perspective on the parent, then animate rotateX and rotateY on the child.
.tilt-wrapper {
perspective: 800px;
}
.tilt-card {
transition: transform 0.1s linear;
transform-style: preserve-3d;
will-change: transform;
}wrapper.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const x = (e.clientY - rect.top) / rect.height - 0.5; // -0.5 to 0.5
const y = (e.clientX - rect.left) / rect.width - 0.5;
card.style.transform = `rotateX(${x * -20}deg) rotateY(${y * 20}deg)`;
});
wrapper.addEventListener('mouseleave', () => {
card.style.transform = 'rotateX(0deg) rotateY(0deg)';
});The 800px perspective value matters a lot. Lower values — say 400px — look exaggerated and toy-like. Higher values — 1200px — feel barely there. 800px is the sweet spot for most cards in the 280px–400px width range. The 20deg max rotation is similarly calibrated: beyond 25deg you start seeing visible distortion on text.
Skew shear (pattern #8) is the brutalist cousin. No JavaScript needed — it's pure CSS and creates the kind of kinetic jolt you see on neobrutalism designs. Pair it with a thick border and a hard offset shadow for maximum impact.
.skew-btn {
transition: transform 0.15s ease;
}
.skew-btn:hover {
transform: skewX(-8deg) translateX(4px);
}9 — Backdrop Blur Reveal and 10 — Filter Chromatic Shift
Pattern #9 is one of my favorites for hero sections. You have a blurred overlay sitting on top of an image, and on hover, the blur *fades out*, revealing the crisp image underneath. It's the opposite of the usual hover — instead of adding an overlay, you're lifting one.
.reveal-card {
position: relative;
overflow: hidden;
}
.reveal-card .blur-layer {
position: absolute;
inset: 0;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: hsl(0 0% 0% / 0.2);
transition: opacity 0.4s ease, backdrop-filter 0.4s ease;
}
.reveal-card:hover .blur-layer {
opacity: 0;
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
}Worth noting: backdrop-filter still requires the -webkit- prefix on Safari as of early 2026. Don't skip it. Also — animating backdrop-filter itself (the blur amount) is supported in Chrome and Safari but not Firefox. Fading opacity on the blurred layer works everywhere and is the safer approach for production.
Pattern #10 is the chromatic shift — a CSS filter trick that gives text or icons that retro RGB-split look on hover. You combine drop-shadow values in red, green, and blue at offset positions to fake channel separation.
.chroma-text {
transition: filter 0.2s ease;
}
.chroma-text:hover {
filter:
drop-shadow(-3px 0 0 hsl(0 100% 60% / 0.8))
drop-shadow(3px 0 0 hsl(200 100% 60% / 0.8));
}This one pairs naturally with the glassmorphism components and anything in the vaporwave style hub — the aesthetic is a perfect match. Keep the offset under 4px or it starts reading as a broken render rather than a deliberate effect. Look, the line between 'stylistic glitch' and 'bug' is genuinely thin here — test it on real users if you're unsure.
FAQ
:hover still fires on tap in most mobile browsers, but there's no mousemove equivalent. For patterns 2 and 7 (magnetic pull and tilt) that rely on mousemove, add a check like window.matchMedia('(hover: hover)') and skip the listener entirely on touch-only devices. The clip-path, underline, and filter effects work fine on tap.
Stick to transform, opacity, filter, clip-path, and backdrop-filter. These are handled by the compositor thread and won't cause layout recalc. Animating width, height, padding, margin, or top/left will cause reflow and can drop frames, especially on lower-end Android devices.
Replace :hover with :hover, :focus-visible on every rule. :focus-visible only shows the focus state when navigating via keyboard, so mouse users won't see a double-state. This covers WCAG 2.4.7 with zero extra work and no visual regression for pointer users.
Yes, but be deliberate. Combining neon glow + perspective tilt works well because they're different property sets. Combining clip-path reveal + skew shear on the same element usually looks chaotic. A good rule: max two animated properties per element, and make sure they read as intentional together rather than competing.