Cool Hover Effect
A CSS-only hover effect for navigation links that slides text up or down based on which half of the link you hover over.
How It Works
The effect uses several clever CSS techniques:
- Grid stacking - The
<span>and pseudo-elements are stacked in the same grid cell usinggrid-area: 1 / 1 - Text shadows - The colored text above and below is created with
text-shadowusing thelh(line-height) unit - Clip-path masking - The link is clipped to hide the shadow text, and pseudo-elements divide the hover zone into top/bottom halves
- Directional detection -
:has(::after:hover)detects when the bottom half is hovered to reverse the animation direction
Live Demo Test
The Code
CSS
.cool-hover-effect {
display: flex;
flex-direction: column;
text-align: center;
}
.cool-hover-effect a {
--clr: #00ffd5;
--pad-y: 0.75rem;
--pad-x: 1rem;
color: white;
text-decoration: none;
font-family: system-ui, sans-serif;
font-size: 2rem;
font-weight: 600;
text-transform: uppercase;
padding: var(--pad-y) var(--pad-x);
display: grid;
clip-path: inset(var(--pad-y) var(--pad-x));
}
/* Top and bottom hover zones using pseudo-elements */
.cool-hover-effect a::before,
.cool-hover-effect a::after {
content: '';
grid-area: 1 / 1;
pointer-events: auto;
}
.cool-hover-effect a::before {
clip-path: inset(0 0 50% 0);
}
.cool-hover-effect a::after {
clip-path: inset(50% 0 0 0);
}
.cool-hover-effect span {
grid-area: 1 / 1;
text-shadow: 0 -1lh var(--clr), 0 1lh var(--clr);
transition: translate 0.3s;
pointer-events: none;
}
/* Default: hover from top, slide down */
.cool-hover-effect a:hover span {
translate: 0 1lh;
}
/* Override: hover from bottom, slide up */
.cool-hover-effect a:has(::after:hover) span {
translate: 0 -1lh;
}
HTML
<nav class="cool-hover-effect">
<a href="#"><span>Neon Drift</span></a>
<a href="#"><span>Shadow Pulse</span></a>
<a href="#"><span>Void Walker</span></a>
<a href="#"><span>Cyber Nexus</span></a>
<a href="#"><span>Phantom Grid</span></a>
</nav>
Key Techniques Explained
The lh Unit
The lh unit equals the computed line-height of the element. This makes the text shadows and translations perfectly match the text size:
text-shadow: 0 -1lh var(--clr), 0 1lh var(--clr);
translate: 0 1lh;
Hover Zone Detection
The ::before and ::after pseudo-elements split the link into top and bottom halves. The :has() selector detects which half is being hovered:
/* Top half */
a::before { clip-path: inset(0 0 50% 0); }
/* Bottom half */
a::after { clip-path: inset(50% 0 0 0); }
/* Detect bottom hover */
a:has(::after:hover) span { ... }
Clip-Path Masking
The padding creates space for the text shadows, but clip-path: inset() hides them until the hover animation reveals them:
padding: var(--pad-y) var(--pad-x);
clip-path: inset(var(--pad-y) var(--pad-x));