Valid HTML, Broken UX
href=”#” and the Focus Trap

Have you ever added a link to your footer to help users navigate back to the top? It usually looks like this:
<a href="#">Back to the top</a>
I’ve seen and used that pattern many times, including on this very website. From an HTML perspective it is absolutely valid (7.4.6.4 Scrolling to a fragment), and I always assumed it was a harmless shortcut that worked well enough.
Then I updated Biome to 2.5 and it flagged it. I opened a bug report expecting to close it quickly, but the answer changed my mind: Biome is not rejecting it because of HTML validity, but because of accessibility behavior.
This Is Not Just a Biome Thing
The rule is actually not new and inspired by other tools that already enforced it:
The difference is scope. So far these checks mostly hit JSX workflows, which is why many of us never stumbled on this in plain HTML. Biome is the first tool (in my daily workflow, at least) that called this out on pure HTML.
What Is Actually Broken?
Focus desynchronization
An empty hash (#) does not point to a specific element ID. When activated, the browser scrolls the page visually, but keyboard focus stays on the trigger element (for example, the footer link you just clicked).
The result is confusing:
- The page looks like it moved to the top.
- Focus did not move with it.
- The next
Tabkeystroke jumps the user back to the bottom context.
WCAG impact
That behavior creates a mismatch between visual reading sequence and interactive focus sequence, which is exactly the kind of failure WCAG 2.4.3 (Focus Order) is trying to prevent.
The Workaround (And Why It Works)
Instead of href="#", give the top container an explicit ID and target it:
<body id="_top">
<!-- page content -->
<a href="#_top">Back to top</a>
</body>
You get the same practical scroll effect, but without focus desync.
See the Pen Empty Fragment Link by th3s4mur41 (@th3s4mur41) on CodePen.
Open “Empty Fragment Link” on CodePen if the embedded preview is not available.
What you will notice:
- The classic empty
#keeps focus on the original footer link after scroll. This is visible through the pink outline and background that stays in the footer. - The explicit fragment target behaves like normal fragment navigation: focus moves with the scroll, the outline disappears from the link and background color is removed.
Although both solutions scroll to the body element, only the second one behaves consistently with any other fragment link pointing to a heading.
Real-World Impact on Screen Reader Users
NVDA Issue #19190 — “Focus shifts to top of page when activating a link using href="#" on Chrome” describes exactly what happens to screen reader users in Chromium-based browsers:
- The viewport jumps to the top.
- The virtual reading context follows that jump.
- Native keyboard focus remains on the original footer element.
- Press
Tab, and you are yanked straight back down.
Why Browser Vendors Haven’t “Fixed” It
This is a classic boundary problem between assistive technology and browser engines, and the issue trail reflects that.
Screen reader side
The NVDA issue was initially closed as not planned — the reasoning being that the screen reader is simply reacting to where the browser moved the viewport. It was later reopened and triaged, and the question of who owns the fix is still open.
Browser engine side
Chromium has addressed a related focus behavior in Issue #40403681: when a fragment identifier points to a non-focusable element, focus handling was improved in that scope.
The empty fragment case (href="#") was not part of that change and can still be reproduced in Firefox and Safari as well.
My Take
What makes this especially frustrating is the asymmetry:
href="#"scrolls the page without moving focus.href="#_top"often targets the same unfocusable element at the top and yet focus behaves correctly.
Same destination. Completely different focus outcomes. That’s hard to defend as intentional design.
I still see this primarily as a browser behavior problem, not a screen reader problem. But because the spec does not define what should happen to focus for an empty fragment navigation, it may need to be updated first.
My expectation is that when navigation to an empty fragment identifier happens, the body element should become the sequential focus navigation starting point.
Conclusion
So the fix on our side is straightforward: use the explicit ID approach. It eliminates an accessibility issue with just one line of code. And it’s a good reminder that “technically valid” and “actually usable” are not the same thing — sometimes you have to work around browser limitations to deliver a coherent user experience.