A snippet to set the Sequential Focus Navigation Starting Point (SFNSP)
A small helper to tell the browser where Tab navigation should start from, without leaving visible focus on the element. Useful in special cases where .focus() or href="#id" isn’t an option.
Disclaimer: This post assumes you have advanced knowledge of frontend development. The topic of Sequential Focus Navigation is quite niche and you should really know what you are doing.
When you open a modal, navigate to a new route in a SPA, or reveal a section of content, keyboard users expect Tab to start from a sensible point inside that new context — not from wherever focus happened to be before.
Native hash links (<a href="#section">) do this automatically: clicking one moves the browser’s Sequential Focus Navigation Starting Point (SFNSP) to the target element. But there’s currently no JavaScript API to do the same thing programmatically. The WHATWG issue tracking this has been open since 2019 — a native element.setSequentialFocusStartingPoint() might be implemented in browsers soon, but isn’t widely available yet.
Until that lands, this snippet is the closest workaround.
This is not a substitute for moving the actual focus. The SFNSP only affects where Tab resumes — natively it does not move document.activeElement, and screen readers largely ignore it. Usually you need screen readers to announce a context change, so you must move focus explicitly. This snippet is for the narrow case where you want Tab to start from a new location without announcing a focus move.
How it works
The trick relies on the browser updating its internal SFNSP whenever an element receives focus. Three steps:
- Set
tabindex="-1"on the target element (so it can be focused programmatically even if it isn’t natively focusable). - Call
.focus()then.blur(), the browser records this element as the new starting point, but focus isn’t left on it. Loggingdocument.activeElementreveals it actually lands on ‘body’. - Restore the original
tabindexso the element’s tab behaviour is unchanged.
The next time the user presses Tab, the browser resumes sequential navigation from this element.
When you might use it
- SPA route changes alongside an announcement mechanism: set the SFNSP to
<main>so Tab enters the new page content, while a separate live region or focus move handles the screen reader announcement. - Closing a non-modal overlay (tooltip, non-modal popover) where returning focus to the trigger would be disruptive: set the SFNSP to the nearest relevant container so Tab continues from a logical point.
- Skip Links to make it easier to go past sections with lots of interactive elements.
- Where a hash link would work but isn’t possible for whatever reason
- Next.js
<Link>with hash targets: see below.
A timing note: call moveSFNSP() only after the target element is in the DOM and painted. Calling it during a transition or before a newly revealed element is rendered will silently fail in most browsers.
Next.js <Link href="#id"> bug
Next.js has an open bug where clicking a hash link does not set the SFNSP — the browser never receives the native anchor navigation event, so the next Tab press resumes from the wrong place.
Vue’s Nuxt partly solved this in NuxtLink.
The workaround is to intercept the click and call moveSFNSP yourself.
// HashLink.tsx
import { type ComponentProps } from "react";
import Link from "next/link";
import { moveSFNSP } from "@/helpers/moveSFNSP";
type HashLinkProps = ComponentProps<typeof Link> & { href: string };
export const HashLink = ({ onClick, ...rest }: HashLinkProps) => {
function handleClick(e: React.MouseEvent<HTMLAnchorElement>) {
try {
const id = decodeURIComponent(rest.href.replace(/^#/, ""));
const target = document.getElementById(id);
if (target) moveSFNSP(target);
} catch {
// ignore decodeURIComponent errors on malformed URIs
}
onClick?.(e);
}
return <Link {...rest} onClick={handleClick} />;
};This restores the behaviour a native <a href="#main"> would give: the next Tab press enters the linked section.
SFNSP and screen readers
The SFNSP is a browser-internal concept. Screen readers do not observe it directly. Sarah Higley’s 2020 testing across NVDA, JAWS, Narrator, VoiceOver, and TalkBack found:
- Tab navigation works correctly in most browser/SR combinations, the next Tab press lands in the right place.
- Virtual/browse cursor (the arrow-key reading mode used by NVDA, JAWS, etc.) largely ignores the SFNSP and either starts over from the top or jumps somewhere unpredictable.
- Context change announcements: no screen reader announced the change of context.
- VoiceOver on macOS was the only combination where both tab and cursor navigation worked as expected.
The practical upshot: relying on SFNSP alone is not sufficient for accessibility. For modals, route changes, or any meaningful context switch, always move visible focus explicitly. This snippet is most useful as a complement to focus management, not a replacement for it.
Demo
Click the button to call moveSFNSP() on the target box, then press Tab or Shift + Tab, the focus should move to the adjacent demo links.
The code: moveSFNSP.ts
Use with caution
This is a hack. It works in current browsers but relies on undocumented internal behaviour. Stick to native href="#id" where you can. Test with a real keyboard and screen reader before shipping.
Copy & paste this to your/project/helpers/moveSFNSP.ts:
/**
* Sets the Sequential Focus Navigation Starting Point (SFNSP) to the given
* element without leaving visible focus on it.
*
* When Tab is next pressed, the browser will start sequential focus navigation
* from this element rather than from wherever focus currently is.
*
* There is no native browser API for this yet — see https://github.com/whatwg/html/issues/5326
* The trick: temporarily assign tabindex="-1", call focus() + blur() to move
* the browser's internal starting point, then restore the original tabindex.
*
* Credit: Rich Harris — https://github.com/whatwg/html/issues/5326#issuecomment-1150090942
*
* @param element - The element to set as the focus starting point.
*/
export function moveSFNSP(element: HTMLElement): void {
const tabindex = element.getAttribute("tabindex");
element.setAttribute("tabindex", "-1");
element.focus({ preventScroll: true });
element.blur();
if (tabindex !== null) {
element.setAttribute("tabindex", tabindex);
} else {
element.removeAttribute("tabindex");
}
}Alternative approach
Philipp Daun’s variant prepends an invisible focusable element, focuses it, then removes it. Apparently the primary approach does not working reliably in all versions of Safari.
function moveSFNSP(element: HTMLElement): void {
const focusEl = document.createElement("div");
focusEl.setAttribute("tabindex", "-1");
focusEl.style.cssText =
"position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);outline:none";
element.prepend(focusEl);
focusEl.focus({ preventScroll: true });
focusEl.remove();
}The downside: Patrick Lauke tests concluded that with JAWS, removing the focused element causes focus to reset to the very start of the page — making the interaction worse than doing nothing. This applies to both variants, but the invisible-element approach may be more likely to trigger it. As always; test thoroughly before using in production.
Further reading
- Sarah Higley: Focus management still matters — detailed AT compatibility results and why SFNSP is not a substitute for explicit focus management.
- WHATWG issue #5326: Sequential focus navigation starting point — the open spec issue tracking a native JS API.
- HTML spec: Sequential focus navigation starting point — how browsers define and use the SFNSP.
- Rob Dodson: Removing Headaches from Focus Management — the thorough primer on SFNSP and same-page anchor behaviour.