---
title: Step‑by‑Step Guide to Managing Dynamic In‑Page Links for Single‑Page Apps
siteUrl: https://logzly.com/toggleanchors
author: toggleanchors (Toggle Anchors)
date: 2026-06-15T20:34:53.832672
tags: [anchors, spa, webdev]
url: https://logzly.com/toggleanchors/stepbystep-guide-to-managing-dynamic-inpage-links-for-singlepage-apps
---


When a user lands on a long page and clicks a menu item, they expect the view to jump right where they need it. In a single‑page app (SPA) that expectation can break if you’re not careful with in‑page anchors. That’s why getting dynamic links right matters now more than ever—users are impatient, and search engines love **[keyboard‑friendly anchor navigation](/toggleanchors/build-keyboardfriendly-anchor-navigation-with-pure-css-and-aria)**.

## Why SPAs Need a Different Anchor Strategy  

A traditional multi‑page site reloads the whole document when you follow a link. The browser automatically scrolls to the element whose `id` matches the hash (`#section`). In an SPA, the page never reloads. Content is often added, removed, or reordered after the initial load, so the hash may point to an element that doesn’t exist yet. If you don’t handle that, the page just sits there, confused.

## The Core Pieces  

1. **Hash change listener** – watches the URL fragment (`window.location.hash`).  
2. **Element registration** – a way for components to tell the app “I’m here, my id is X”.  
3. **Smooth scrolling logic** – optional but makes the experience feel polished.  
4. **Accessibility check** – focus must move to the target element so screen readers announce it.

## Step 1: Set Up a Central Anchor Manager  

Create a tiny module that stores a map of ids to DOM nodes. Here’s a plain‑JS example:

```js
// anchorManager.js
const anchors = new Map()

export function registerAnchor(id, element) {
  if (id && element) {
    anchors.set(id, element)
  }
}

export function unregisterAnchor(id) {
  anchors.delete(id)
}

export function getAnchor(id) {
  return anchors.get(id)
}
```

Every component that renders a section can call `registerAnchor` in a `useEffect` (React) or `connectedCallback` (Web Components). When the component unmounts, call `unregisterAnchor` to keep the map clean.

## Step 2: Listen for Hash Changes  

Add a listener once, preferably at the root of your app:

```js
// scrollHandler.js
import { getAnchor } from './anchorManager.js'

function scrollToHash() {
  const hash = window.location.hash.slice(1) // remove the #
  if (!hash) return

  const target = getAnchor(hash)
  if (target) {
    target.scrollIntoView({ behavior: 'smooth', block: 'start' })
    // Move keyboard focus for accessibility
    target.setAttribute('tabindex', '-1')
    target.focus()
  }
}

// Run on load in case the URL already has a hash
window.addEventListener('DOMContentLoaded', scrollToHash)
window.addEventListener('hashchange', scrollToHash)
```

The `scrollIntoView` call works in all modern browsers and gives you a smooth scroll without extra libraries.

## Step 3: Register Anchors in Your Components  

If you’re using React, a tiny hook does the job:

```js
// useAnchor.js
import { useEffect, useRef } from 'react'
import { registerAnchor, unregisterAnchor } from './anchorManager.js'

export function useAnchor(id) {
  const ref = useRef(null)

  useEffect(() => {
    if (ref.current) {
      registerAnchor(id, ref.current)
    }
    return () => {
      unregisterAnchor(id)
    }
  }, [id])

  return ref
}
```

Then in a component:

```js
function Section({ id, title, children }) {
  const ref = useAnchor(id)

  return (
    <section id={id} ref={ref}>
      <h2>{title}</h2>
      {children}
    </section>
  )
}
```

The same idea works in Vue, Svelte, or plain JavaScript—just make sure the element is registered after it appears in the DOM. **If you also need to support users who navigate via the keyboard, our guide on [building keyboard‑friendly anchor navigation with pure CSS and ARIA](/toggleanchors/build-keyboardfriendly-anchor-navigation-with-pure-css-and-aria) offers complementary techniques.**

## Step 4: Handle Dynamic Content Loading  

Sometimes a section is loaded only after a network request. In that case, the hash may already be in the URL when the data arrives. After you render the new content, call `scrollToHash` manually:

```js
import { scrollToHash } from './scrollHandler.js'

async function loadSectionData(id) {
  const data = await fetch(`/api/section/${id}`).then(r => r.json())
  // render data...
  // once the DOM node exists:
  scrollToHash()
}
```

Because the manager already knows about the new element, the scroll will happen instantly.

## Step 5: Keep SEO and Accessibility Happy  

Even though SPAs rely on JavaScript, search engines still read the static HTML that’s sent initially. Make sure each anchorable section has a real `id` attribute in the markup. That way a crawler can see the structure without running JS.

For accessibility, always move focus to the target element after scrolling. Adding `tabindex="-1"` lets you focus an element that isn’t normally focusable (like a `<div>`). Screen readers will then announce the heading or content inside. **For a deeper dive into making anchors both accessible and interactive, see the [seamless keyboard‑friendly navigation tutorial](/toggleanchors/seamless-keyboardfriendly-anchor-navigation-with-css-and-a-little-javascript) that blends CSS with a little JavaScript.**

## Common Pitfalls and How to Avoid Them  

| Problem | Why it Happens | Fix |
|---|---|---|
| Scroll jumps to top | `hashchange` fires before the element is in the DOM | Delay the scroll until after registration, or use a `setTimeout` of 0 as a quick hack |
| Multiple elements share the same id | Duplicate ids break the map lookup | Enforce unique ids, perhaps by prefixing with the page name |
| Focus ring disappears | Browser removes focus after smooth scroll | Keep `tabindex="-1"` on the target, or add a CSS rule to show focus on programmatic focus |

## Quick Checklist for Every New SPA Page  

- [ ] Every section that should be reachable has a unique `id`.  
- [ ] The component registers itself with the anchor manager.  
- [ ] A global hash listener calls `scrollIntoView` with smooth behavior.  
- [ ] After dynamic loads, invoke the scroll function again.  
- [ ] Focus is moved to the target element for screen readers.  

Follow this checklist and you’ll have in‑page navigation that feels as fast as a native app, works for assistive tech, and stays SEO‑friendly.