Valid HTML, Broken UX

href=”#” and the Focus Trap

7 min read • 781 words

A comic style illustration within a web browser frame shows Michaël Vanderheyden looking confused as a giant diagonal lightning bolt splits the scene. His upper body is shown floating up towards a yellow background box reading 'VIEWPORT: SCROLL TO TOP,' while his lower body and legs are physically chained to a rusty anchor labeled 'keyboard focus trapped in footer' keycap-rubble background

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:

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.

Interactive CodePen demo with two footer links: one with href='#' and another with href='#_top' to demonstrate the focus desynchronization issue and its workaround.
Open “Empty Fragment Link” on CodePen if the embedded preview is not available.

What you will notice:

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:

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:

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.