You have seen this effect, even if you rarely notice it consciously: you hover over a navigation menu, the item turns bold, and the whole menu twitches. The neighbours shift a few pixels to the side, a line suddenly wraps differently, a right-aligned number hops. It is only a detail, but it makes a page feel restless and cheap.
This post explains why it happens and how a simple text-shadow makes text look bold without moving a single pixel of the layout.
Why bold text jumps
A bold typeface is not just “the same letters with thicker strokes”. Bold weights have wider glyphs and wider spacing: an “m” in bold is noticeably wider than an “m” in regular. So when you write
.nav a:hover {
font-weight: 700;
}
the link gets wider. In a row with display: flex or inline, it pushes every following element to the right. In a box with a fixed width it may even wrap, and then its height changes too.
Hover the items. The red lines mark where each item starts at rest.
Typical places where this hurts:
- Navigation bars and tabs (
:hover, the active tab) - Lists with a selection, such as folders or filters in a sidebar
- Buttons whose width depends on their text
- Table rows that are highlighted on hover
The idea: fake the bold
The trick is old, much older than CSS. Typewriters and dot-matrix printers often produced bold text this way: strike the same character again, offset by a tiny amount. The result looks heavier but takes up exactly the same space.
CSS has a tool with exactly this property: text-shadow. Shadows take up no space in the layout. They change neither the width nor the height of a box and never cause a line to wrap. Give the shadow the text colour and offset it by a fraction of a pixel, and it merges with the letters, making every stroke slightly thicker.
.nav a:hover,
.nav a[aria-current="page"] {
text-shadow: 0.5px 0 0 currentColor;
}
That is essentially all there is to it. currentColor makes sure the “shadow” always has the colour of the text, in dark mode too, or when the link colour changes on hover.
Hover the items. The red lines mark where each item starts at rest.
Fine-tuning
A single shadow to the right thickens the strokes asymmetrically. For a more even look, spread two shadows to both sides:
.nav a:hover {
text-shadow:
0.35px 0 0 currentColor,
-0.35px 0 0 currentColor;
}
Pick the value to suit the font size and the typeface:
| Font size | Offset (per side) |
|---|---|
| up to about 14px | 0.25px–0.35px |
| 16px–20px | 0.35px–0.5px |
| large headings | 0.5px–1px |
These are starting points to experiment with, not fixed rules. On high-resolution displays small fractions look finer; on regular monitors they get smoothed out more. Check on both.
Hover the items. The red lines mark where each item starts at rest.
A smooth transition with ease
A nice side effect: text-shadow can be animated. With a transition, the text gains weight smoothly on hover and loses it again on leave, instead of switching abruptly:
.nav a {
transition: text-shadow 0.25s ease;
}
.nav a:hover {
text-shadow:
0.4px 0 0 currentColor,
-0.4px 0 0 currentColor;
}
The important part is that the transition sits on the element at rest, not only in :hover. Only then is the way back animated too. For it to look smooth, the resting state also needs a comparable value. Without any text-shadow, the browser interpolates from “no shadow”, which works in current browsers, but text-shadow: 0 0 0 currentColor as a starting value makes it explicit.
150 to 300 milliseconds is enough. Shorter feels like a hard switch, longer feels sluggish, especially when you move quickly across several items. ease starts fast and settles softly, which suits hover effects well. ease-out is a good alternative.
With font-weight and classic static fonts this does not work: the value simply jumps from 400 to 700. And even when a variable font can animate the weight continuously, the width grows along with it: the text no longer jumps, it slides, and its neighbours slide too.
Hover the items. The red lines mark where each item starts at rest.
Your system asks for reduced motion, so the transition is switched off here (prefers-reduced-motion: reduce).
Respecting reduced motion
Some people are sensitive to motion on screen, for example with migraines, dizziness or vestibular disorders. Every operating system has a setting for them that reduces animations, and the browser exposes it through the prefers-reduced-motion media query. A page that honours it switches transitions off for these users:
@media (prefers-reduced-motion: reduce) {
.nav a {
transition: none;
}
}
The effect itself stays: the text still turns bold, just instantly instead of fading in. That is the point: reduce means “less motion”, not “no highlight”.
A subtle shadow over a quarter of a second is hardly a demanding animation. But the media query costs three lines, and if you write it for small effects from the start, you won’t forget it for big ones.
You don’t have to change your system settings to test it:
- Chrome and Edge: DevTools → “More tools” menu → Rendering → “Emulate CSS media feature prefers-reduced-motion” →
reduce - Firefox:
about:config→ setui.prefersReducedMotionto1 - System-wide: Windows 11: Settings → Accessibility → Visual effects → Animation effects off; Windows 10: Settings → Ease of Access → Display → “Show animations in Windows” off; macOS: Accessibility → Display → Reduce motion
A complete example
<nav class="nav">
<a href="/" aria-current="page">Home</a>
<a href="/articles">Articles</a>
<a href="/projects">Projects</a>
<a href="/contact">Contact</a>
</nav>
.nav {
display: flex;
gap: 1.5rem;
}
.nav a {
color: inherit;
text-decoration: none;
transition: text-shadow 0.25s ease;
}
.nav a:hover,
.nav a:focus-visible,
.nav a[aria-current="page"] {
text-shadow:
0.4px 0 0 currentColor,
-0.4px 0 0 currentColor;
}
@media (prefers-reduced-motion: reduce) {
.nav a {
transition: none;
}
}
Hover over “Articles” and it gets heavier, while “Projects” and “Contact” stay exactly where they are.
Hover the items. The red lines mark where each item starts at rest.
The complete example, “Home” is the current page
Limits
The trick is not a full replacement for a real bold weight:
- It is an imitation. A real bold weight is drawn by the type designer, with adjusted counters and proportions. The shadow only thickens all strokes evenly. That is usually enough for a highlight, but not for whole paragraphs in bold.
- Too much smears. With large values the counters of “e”, “a” or “g” fill in and the text turns mushy.
- Accessibility: in forced colors mode (for example Windows contrast themes), browsers remove
text-shadow. The highlight then disappears. If you rely on it, for instance to mark the current menu item, show a different cue there:
@media (forced-colors: active) {
.nav a[aria-current="page"] {
text-decoration: underline;
}
}
And in general: the current menu item should never be recognisable by stroke weight alone. aria-current="page" helps screen readers, and an underline or marker helps everyone else.
Alternatives
The shadow is not the only way, but often the simplest. Briefly, the others:
Reserve the space for the bold width. Render the text a second time, invisibly and in bold, so the element has the bold width from the start:
.nav a {
display: inline-flex;
flex-direction: column;
}
.nav a::after {
content: attr(data-text);
font-weight: 700;
height: 0;
overflow: hidden;
visibility: hidden;
}
.nav a:hover {
font-weight: 700;
}
This looks genuinely bold, but needs the text twice in the HTML (data-text="Articles"), and the items take up the wider bold width even when not hovered. Left-aligned text also visibly “grows” to the right into the reserved space on hover.
Hover the items. The red lines mark where each item starts at rest.
Reserved bold width (::after): no jump, but wider at rest
–webkit-text-stroke. An outline around the letters, also without any effect on the layout. But it sits centred on the outline and fills in the counters at small sizes faster than a shadow does.
Variable fonts with a grade axis. Some variable fonts (such as Roboto Flex) have a GRAD axis that changes the stroke weight without changing the width:
.nav a:hover {
font-variation-settings: "GRAD" 150;
}
This is the cleanest solution, but it only works with fonts that actually have this axis. The regular wght axis, on the other hand, changes the width just like a static bold weight.
Hover the items. The red lines mark where each item starts at rest.
Conclusion
If text must not jump when it is highlighted, font-weight is the wrong tool, because it changes the width. A text-shadow in currentColor, offset by a fraction of a pixel, makes text look bold without touching the layout. It takes one line of CSS, can be animated and works with any typeface. For real, longer bold passages the bold weight remains the right choice; for hover and selection states, the shadow is almost always the better one.

Leave a Reply