A curated collection of modern, copy-ready CSS link hover effects, from subtle to expressive.
A CSS link hover effect is a visual change applied to an anchor element the instant a pointer rests over it.
The browser triggers that change through the :hover pseudo-class selector, one of the state-based selectors built into CSS.
Nothing about it requires JavaScript. The entire behavior lives inside a stylesheet rule, which is part of why it's one of the cheapest interactions to add to a page.
Hover state and hover effect are two different things. The state is the condition (a pointer sitting over the element), the effect is whatever changes because of it: color, underline, size, position, or some mix of all four.
Common categories show up across nearly every production site:
Color shifts on text or background
Underlines that slide, fade, or grow in
Background fills that sweep in from one edge
Transform-based movement (scale, translate, skew)
Icon or arrow motion paired with text
Hover effects sit inside the broader practice of building interactive elements into a page, and most design teams now treat them as a form of micro-interaction rather than pure decoration.
MDN's documentation notes the :hover pseudo-class has been available across every major browser since July 2015, making it one of the most stable interaction hooks CSS has ever shipped.
The New York Times applies a restrained version of this on its article pages. A thin underline appears beneath in-text links only on hover, signaling that something is clickable without disrupting the reading flow around it.
The :hover pseudo-class targets an anchor with simple syntax: a:hover { }.
Any property declared inside that block applies only while the pointer sits over the link, and reverts the instant it leaves.
CSS treats :hover as one of three interaction pseudo-classes tied to the HTML anchor element, alongside :focus and :active.
Each pseudo-class fires under a different condition, and mixing them up is a recurring mistake across CSS link styles in production code.
Pseudo-class | Trigger | Typical device |
|---|---|---|
:hover | Pointer rests over the element | Mouse, trackpad |
:focus | Element receives keyboard or tab focus | Keyboard, screen reader |
:active | Element is currently being clicked or tapped | Mouse, touch |
Keyboard users never trigger :hover at all. A link that only changes appearance on hover is functionally invisible to anyone tabbing through a page.
That gap matters more than it looks. In WebAIM's low vision survey, 60.4% of respondents said they always or often use a keyboard to navigate the web, and none of them will ever see a hover-only effect fire.
CSS applies the last matching rule when specificity ties, so the order pseudo-classes appear in a stylesheet decides which one wins.
LVHA is the standard memory aid: :link, :visited, :hover, :active.
:link styles unvisited links
:visited styles links the browser has already recorded as clicked
:hover needs to come after both, or a visited link's color can silently override the hover state
:active comes last so a click always shows feedback, even mid-hover
Get the order wrong and a hover effect can pass every manual test, then quietly stop firing the moment a link enters the visited state.
Bootstrap's base stylesheet defines its default anchor states in this exact LVHA sequence, which is part of why teams that override those link styles without preserving the order run into production bugs nobody catches in review.
The transition property is what turns an instant style swap into a smooth change.
Four sub-properties control it: transition-property, transition-duration, transition-timing-function, and transition-delay.
Shorthand covers all four in one line: transition: color 0.3s ease;.
Without it, every hover effect snaps between states with zero animation, however dramatic the underlying style change happens to be.
That's a different mechanism from CSS keyframes, which loop and sequence through steps independent of any user action.
A transition only ever moves between two states: default and hover.
Nielsen Norman Group's animation research puts the useful range for interface motion at 100 to 400 milliseconds, noting that a tenth of a second can change how an interaction feels to the person triggering it.
Hover transitions in real codebases cluster tighter than that full range, typically landing between 150ms and 300ms.
Under 100ms: barely perceptible, reads as instant
150ms to 300ms: the sweet spot for most link and button hovers
Over 400ms: starts to feel sluggish, especially on menus users hover repeatedly
The same research points to Google's Material Design toggle switch as a well-tuned example: a 100ms transition covers the button's position, its color shift, and a fading halo effect all at once.
ease, linear, and cubic-bezier() produce visibly different motion even at an identical duration.
Curve | Feel | Common use |
|---|---|---|
linear | Constant speed, mechanical | Loading bars, spinners |
ease | Slow start, fast middle, slow end | Default browser hover behavior |
ease-in-out | Symmetrical acceleration | Larger UI shifts, modals |
cubic-bezier() | Fully custom curve | Branded or signature motion |
ease is the CSS default whenever no timing function gets specified, which explains why most untouched hover transitions already feel reasonably natural.
Custom curves need a visual reference to tune correctly instead of guessing coordinate values by hand. A CSS transition generator speeds that process up considerably compared to editing four bezier numbers blind.
Underlines carry the strongest visual association with "this is a link" of any hover technique, which is why so many implementations still revolve around them.
Three approaches dominate: native text-decoration, a ::after pseudo-element animated with transform: scaleX(), and a background-image sweep.
Technique | How it works | Multi-line support |
|---|---|---|
text-decoration | Native browser underline, color and thickness controllable | Yes |
::after + scaleX() | Pseudo-element line animated with transform | No, needs an inline element |
background-image sweep | Gradient background animated through background-position | Yes |
Copyblogger's research on call-to-action design found that visually distinguishing a clickable link, underlines included, can lift click-through by as much as 200% compared to text that blends into its surroundings.
That's a different scope than CSS button hover effects, where the whole element already reads as clickable before any hover state fires.
The ::after pseudo-element approach adds a line without touching the HTML markup at all.
a::after {
content: "";
position: absolute;
width: 100%;
height: 2px;
bottom: 0;
left: 0;
background-color: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
a:hover::after {
transform: scaleX(1);
}
transform-origin sets the reveal direction. Left produces a left-to-right sweep, center reveals outward from the middle, right reverses the whole thing.
Tobias Ahlin, a design engineer who has worked on product teams at Spotify and Minecraft, published close to this exact technique on his blog. It has since become close to the standard reference implementation for animated underlines.
A simpler variant skips the pseudo-element entirely and animates border-bottom directly, though this shifts surrounding layout unless padding compensates for the added thickness.
Set a transparent 2px border-bottom in the default state
Swap the border color on :hover
Pair it with box-sizing: border-box so the border doesn't push neighboring content
Apple runs a close relative of this pattern across its site navigation. Links carry a thin gray underline by default that switches to blue only on hover, built almost entirely from CSS border animation rather than JavaScript.
Border-based techniques generally render a touch faster than pseudo-element ones, since they skip the extra layer the browser has to composite, though the gap is negligible for a handful of navigation links.
A background or color fill effect swaps the background color, sweeps in a directional fill, or shifts text color the moment a link is hovered.
The simplest version transitions background-color directly. The more elaborate version animates background-position across an oversized gradient to fake a directional sweep.
This technique sets a background twice the element's width, half one color and half another, then slides it into view on hover by transitioning background-position.
a {
background-image: linear-gradient(currentColor, currentColor);
background-size: 0% 2px;
background-position: left bottom;
background-repeat: no-repeat;
transition: background-size 0.3s ease;
}
a:hover {
background-size: 100% 2px;
}
Uniqlo used a related version of this idea on its product hover boxes. A striped pattern fades and animates in using background-position, since there's no native way to transition a background's opacity on its own.
Padding matters here more than in most other hover techniques. Without breathing room around the text, a directional fill can visually clip against neighboring elements before the animation finishes.
WCAG Success Criterion 1.4.3 sets a minimum contrast ratio of 4.5:1 for normal text against its background, and 3:1 for large-scale text (18pt or 14pt bold and above).
That requirement doesn't pause during a hover state. WebAIM's guidance is explicit on this: text that changes color contrast on hover or focus has to meet the same ratio in every state it can appear in, not just the default one.
Check the default state against its background
Check the hover state against its (possibly new) background
Check the focus state separately, since it can differ from both
A fill effect that looks fine in its resting state can quietly fail accessibility testing the moment it animates in a lighter or darker background behind the same text color.
A text transform hover effect changes an anchor's geometry rather than its color: scale, position, spacing, or skew.
These effects run on the CSS transform property, which behaves differently from properties like width or margin because it doesn't affect surrounding layout while it animates.
scale() grows or shrinks text on hover, while translateY() or translateX() shifts it along one axis without disturbing the elements around it.
transform: scale(1.05) for a subtle growth effect
transform: translateY(-2px) for a lift, as if the text is rising toward the pointer
Combine both in one transform declaration for a lift-and-grow effect together
transform and opacity are the two cheapest properties to animate in a browser. Both can run entirely on the compositor thread, which means the animation stays smooth even if the main thread is busy with something else, a distinction web.dev's rendering performance research covers in detail.
Nielsen Norman Group's same interface-animation research recommends staying inside that familiar 100 to 400 millisecond window here too. Scale effects that run longer start to feel like the text is struggling to catch up with the pointer.
Letter-spacing expansion widens the gaps between characters on hover, one of the simpler entries in the broader CSS text animation toolkit, most often paired with all-caps navigation text or button labels.
a {
letter-spacing: normal;
transition: letter-spacing 0.25s ease;
}
a:hover {
letter-spacing: 0.05em;
}
Small values do most of the work. Anything past roughly 0.1em starts to break word recognition rather than add polish, since the eye has to work harder to parse the widened gaps.
This effect reads best on short labels ("Shop", "About", "Contact") rather than full sentences, where the added spacing can noticeably change how many lines a paragraph wraps into.
An icon or arrow motion hover effect pairs a small graphic, usually an arrow, with link text, then animates that graphic on hover to reinforce the idea of forward movement.
The two dominant techniques are a translateX() slide for simple arrows and an SVG stroke-dashoffset animation for a hand-drawn reveal effect.
The slide effect nudges an arrow a few pixels to the right the moment a "read more" or "learn more" link is hovered.
a .arrow {
display: inline-block;
transition: transform 0.2s ease;
}
a:hover .arrow {
transform: translateX(4px);
}
This is a transform-only animation, which is exactly why it stays smooth.
Because translateX() only triggers the compositor step of the rendering pipeline, it skips the layout and paint work that full-property animations require.
The same performance principle applies to CSS arrows built with pure border tricks instead of icon fonts or SVGs.
Adrián Gubrica's portfolio site uses a close relative of this idea: hovering over inline project thumbnails reveals a large arrow icon, the universal cue that a click will open something external.
An SVG stroke draw effect animates the stroke-dashoffset property so a line appears to draw itself in on hover, rather than simply fading or sliding into place.
Set stroke-dasharray to the path's total length
Set stroke-dashoffset to that same value by default, hiding the stroke
Transition stroke-dashoffset to 0 on :hover to reveal the path
This technique needs the path's exact length in pixels to work correctly, which usually means checking the value in browser DevTools rather than guessing it.
Guides on how to animate SVG with CSS cover this exact property pairing in more depth, since stroke-dasharray and stroke-dashoffset behave a little differently from every other CSS animation trick covered above.
Gradient and layered hover effects push past flat color changes into multi-step visual depth: shifting gradients, glowing shadows, and stacked backgrounds.
Both techniques trade some performance for visual richness, so they work best on a handful of hero links rather than an entire navigation menu.
Technique | Core property | Performance cost |
|---|---|---|
Animated gradient text | background-clip, background-position | Low to moderate |
Box-shadow glow | box-shadow | Moderate, triggers paint |
Gradient text works by clipping a background gradient to the shape of the text itself, using background-clip: text with the text color set to transparent.
h2 {
background: linear-gradient(90deg, #7953cd, #00affa);
background-clip: text;
color: transparent;
transition: background-position 0.4s ease;
}
background-clip itself can't be animated. Every gradient text hover effect actually transitions background-position or background-size on an oversized gradient instead.
Google's Gemini landing page uses a version of this technique for its animated headline text, sliding a gradient across the letters rather than relying on a static fill.
A CSS gradient generator makes it easier to preview color stops before wiring up the transition.
A glow effect stacks two or more box-shadow values, then increases their blur radius and opacity on hover to fake a light source.
Small, tight shadow for the base depth
Larger, blurred shadow layered on top for the glow itself
Both transition together through one transition: box-shadow declaration
Unlike transform and opacity, box-shadow triggers a paint step, so glow effects cost more to render than a simple underline or scale animation.
That cost is manageable on a handful of cards or buttons. Full CSS shadow effects collections are worth reviewing before committing to a heavier layered look across an entire grid of elements.
A hover effect only helps if every visitor can trigger it, see it, and dismiss it.
WCAG 2.1 Success Criterion 1.4.13 sets three requirements for any content that appears on hover: it must be dismissible, hoverable, and persistent, meaning it doesn't vanish before someone has time to read it.
Dismissible: closeable without moving the pointer or keyboard focus, usually with Escape
Hoverable: the pointer can move onto the revealed content without it disappearing
Persistent: content stays visible until the trigger, or the user, dismisses it
That standard sits at Level AA, the bar most web accessibility audits and legal requirements get measured against.
Pairing :hover with :focus-visible is what makes an effect usable for both a mouse and a keyboard.
a:hover,
a:focus-visible {
text-decoration-color: currentColor;
}
:focus-visible reached broad browser support faster than most CSS features. Chrome shipped it in version 86 (October 2020), Firefox in version 85 (January 2021), and Safari followed in version 15.4 (March 2022).
The UK government's GOV.UK Design System pairs every interactive style with a visible focus indicator by default, a deliberate choice given the accessibility standards public sector sites are required to meet.
93% of global browser users are on a version that supports prefers-reduced-motion, based on caniuse data.
@media (prefers-reduced-motion: reduce) {
a {
transition: none;
}
}
That single media query lets a hover effect stay visually rich for most people while dropping motion entirely for anyone with a vestibular disorder or motion sensitivity.
A full web accessibility checklist covers this alongside contrast, focus order, and keyboard traps, all of which interact with how a hover effect ultimately behaves.
Not every CSS property costs the same to animate.
Browsers render each frame in three possible stages, style, layout, and paint, before finally compositing everything to the screen.
Property type | Examples | Rendering stage triggered |
|---|---|---|
Compositor-only | transform, opacity | Composite (cheapest) |
Paint-triggering | box-shadow, background-color, border-radius | Paint |
Layout-triggering | width, height, top, margin | Layout (most expensive) |
Motion.dev's rendering research places transform, opacity, filter, and clip-path in its top performance tier: styles that can run entirely on the compositor thread, independent of whatever the main thread is doing.
A 60fps animation needs every frame finished in 16.7 milliseconds. Web.dev's performance guidance is direct on this: animate anything that triggers layout, and hitting that budget gets difficult fast, especially on lower-powered phones.
Chrome DevTools' Rendering tab includes a paint-flashing overlay that highlights exactly which elements repaint during a hover animation, a fast way to catch an expensive property before it ships.
A CSS Animation Generator is useful for previewing keyframe-based motion, though a plain transition on transform or opacity still outperforms it for a simple hover state.
Touch devices don't have a pointer that "hovers" the way a mouse does, which forces browsers to guess at what a tap should trigger.
58.3% of global web traffic came from mobile devices as of 2025, according to StatCounter, ahead of desktop's 39.8% share.
That majority makes touch behavior a default concern, not an edge case.
MDN documents the inconsistency directly: on a touchscreen, :hover might never match, might match briefly after a tap, or might stay matched until another element is touched.
iOS Safari in particular often needs a tap-and-hold before :hover registers at all, a behavior commonly nicknamed sticky hover.
The hover and pointer media features let a stylesheet detect whether the primary input device can actually hover.
@media (hover: hover) and (pointer: fine) {
a:hover {
text-decoration-color: blue;
}
}
hover: hover targets devices where hovering is possible without extra effort
pointer: fine targets precise pointers like a mouse or trackpad
hover: none and pointer: coarse catch touchscreens, where a fallback on :active usually works better than :hover
These features have shipped in every major browser engine for several years, though Smashing Magazine's own testing notes inconsistent edge cases on hybrid devices like Surface tablets and Smart TVs.
Most core hover techniques, :hover, transition, and transform, have full support across every browser still in meaningful use.
The gaps show up in the newer accessibility and visual features layered on top of them.
Feature | Supported since | Note |
|---|---|---|
:focus-visible | Chrome 86, Firefox 85, Safari 15.4 | Full baseline reached by March 2022 |
prefers-reduced-motion | Safari 10.1, Firefox 63, Chrome 74 | Full desktop coverage by April 2019 |
background-clip: text | Long-standing with -webkit- prefix | Unprefixed support still inconsistent |
Safari and Firefox still have documented bugs around background-clip: text, according to CSS-Tricks' own testing, which found Chrome to be the only browser with fully solid support for the property.
Including the -webkit- prefix alongside the standard property remains the safer default for gradient text, even on browsers that technically support the unprefixed version.
Can I Use stays the fastest way to confirm current numbers before shipping any hover technique to production, since browser support shifts version to version in ways a single article can't track in real time.
Teams maintaining a defined browser support matrix, the kind covered under cross-browser compatibility testing, typically treat gradient text and layered shadows as progressive enhancements rather than requirements.
A CSS link hover effect is a style change applied to an anchor element the moment a pointer rests over it, triggered by the :hover pseudo-class.
No JavaScript required, just a stylesheet rule reacting to pointer position.
Target the anchor with a:hover { } and declare whatever property should change: color, background, transform, or text-decoration.
Add a transition on the base a rule, not just :hover, so the change animates smoothly instead of snapping instantly.
The most common cause is specificity: another rule with equal or higher weight overrides the hover style. Wrong pseudo-class order is the second most common culprit.
A CSS specificity calculator confirms which rule actually wins.
:hover applies while the pointer simply rests over an element. :active applies only during the actual click or tap, the brief moment the mouse button is pressed down.
Many buttons use both together for layered feedback.
Yes, virtually all hover effects run entirely on CSS, no JavaScript required. The :hover pseudo-class combined with transition and transform covers color changes, underlines, background fills, scaling, and icon motion on its own.
Most interface research points to 100 to 400 milliseconds, with 150ms to 300ms working best for simple link and button hovers.
Anything longer starts to feel sluggish, a range consistent with broader CSS animation timing guidance across interfaces.
Inconsistently. iOS Safari often needs a tap-and-hold to register :hover, and some browsers keep the state stuck until another element is tapped.
This is part of why responsive design treats touch input differently from mouse and trackpad pointer behavior.
Pair every :hover rule with :focus (or :focus-visible), since keyboard navigation never triggers a pointer-based hover state at all.
Without that pairing, anyone tabbing through the page misses the visual cue that a link is currently selected.
ease is the safest default and already feels natural for most color and underline changes.
cubic-bezier() is worth the extra setup for branded motion, while linear almost never suits a hover effect since it feels mechanical.
Yes, the simplest method uses a ::after pseudo-element scaled with transform: scaleX(), sliding in from either edge on hover.
Multi-line text needs a different approach, usually a background-image gradient animated through background-position instead.