/ โ† Internal Link Fixes / Anchor links not working
In this guide
  1. How an anchor link finds its target
  2. Common causes and how to spot them
  3. How to diagnose a broken jump link
  4. The key HTML and CSS changes
  5. Platform notes
  6. A worked example
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping jump links working
  10. Questions and answers

How an anchor link finds its target

An anchor link is an ordinary link whose address ends in a fragment: a # followed by a name. When the browser follows it, it looks for an element on the page whose id equals that name, scrolls so that element sits at the top of the viewport, and adds the fragment to the address bar.

<a href="#getting-started">Getting started</a>
...
<h2 id="getting-started">Getting started</h2>

The lookup is strict. The name is case-sensitive, it must match the whole id, and only the first element with that id is used. If no element has the id, the browser falls back to an <a> element with a matching name attribute, which is how pages were written years ago. That fallback still works, but the name attribute on links is obsolete in current HTML, so new pages should always use an id on the element itself.

If nothing matches at all, nothing happens. The address bar changes, the page stays where it is, and there is no error in the console. That silence is why broken anchor links can go unnoticed for months.

๐Ÿ’ก Fragments are never sent to the server. Server logs and redirects cannot see the part after #, so a redirect from an old page keeps the fragment only if the browser carries it over, which modern browsers do when the redirect target has none of its own.

Common causes and how to spot them

SymptomLikely causeQuick check
Nothing happens on clickNo element with that id, or a case or spelling mismatchdocument.getElementById('name') returns null
Jumps to the wrong sectionDuplicate id earlier on the pageCount elements with that id
Jumps, but the heading is hiddenFixed or sticky header covers the targetScroll up slightly and the heading is there
Works on reload, not on first visitTarget built by JavaScript after loadView source does not contain the id
Goes to the home page insteadA <base href> tag changes what #name resolves toSearch the head for <base
Works with JavaScript off onlySmooth-scroll script, slider, tabs or router catching the clickDisable JavaScript and retry
Lands short or overshootsImages above the target load late and push it downImages lack width and height
Target inside a closed accordionThe element is hidden, so there is nothing to scroll toOpen the accordion and retry

Two of these deserve a closer look. Duplicate ids are common on pages assembled from repeated blocks, such as a page builder section saved and reused, or a product card template that gives every card id="details". Our guide to duplicate ids covers how to find and remove them. The <base> problem is less common but confusing: with <base href="https://example.com/"> in the head, a link written as #plans resolves to the home page with that fragment, so the browser leaves the current page entirely.

How to diagnose a broken jump link

Start with the quickest test: turn JavaScript off and try the link. In Chrome, open DevTools, press Ctrl+Shift+P (Cmd+Shift+P on a Mac), type Disable JavaScript and press Enter, then reload. If the link works now, a script is the cause. If it still fails, the problem is in the HTML or CSS.

Next, check every same-page link against the ids that exist. Paste this into the console:

const links = [...document.querySelectorAll('a[href*="#"]')]
  .filter(a => a.pathname === location.pathname && a.hash.length > 1);

links.forEach(a => {
  const id = decodeURIComponent(a.hash.slice(1));
  const found = document.querySelectorAll('[id="' + CSS.escape(id) + '"]').length;
  if (found !== 1) console.warn(found ? 'Duplicate id' : 'Missing target', a.hash, a);
});

Every warning is a link that cannot work reliably. A missing target usually means a typo, a capital letter or an id that was renamed when a heading was edited. A duplicate means the browser goes to the first copy, which may not be the one you meant.

To find every duplicate id on the page, not only those used by links:

const seen = {};
document.querySelectorAll('[id]').forEach(el => seen[el.id] = (seen[el.id] || 0) + 1);
console.table(Object.entries(seen).filter(([, n]) => n > 1));

Finally, compare the inspected page with View Source. If the id appears in the Elements panel but not in the source, the section is created by JavaScript, and the browser may not find it at the moment the page first loads with a fragment in the address.

The key HTML and CSS changes

Write stable, simple ids

Use lower-case letters, numbers and hyphens, with no spaces, and make each id unique on the page. An id must not contain whitespace in HTML, and spaces encoded as %20 are a common source of mismatches. Keep ids stable once published, because other sites, emails and search results may link to them.

<nav aria-label="On this page">
  <a href="#pricing">Pricing</a>
  <a href="#delivery-times">Delivery times</a>
</nav>
<h2 id="pricing">Pricing</h2>
<h2 id="delivery-times">Delivery times</h2>

Allow for a sticky header

If your header stays fixed at the top, the browser scrolls the target to the very top of the viewport, underneath the header. Tell it to leave room:

/* Space above every element that is a jump target */
:target, h2[id], h3[id] { scroll-margin-top: 80px; }

/* Or set the offset once for the whole page */
html { scroll-padding-top: 80px; }

Match the value to the header's height, and use a media query if the header is shorter on phones. Both properties are supported in all current browsers.

Smooth scrolling without a script

Many sites add a jQuery plugin just to animate the jump. CSS can do it natively with its smooth scrolling property, set on the html element. Wrap that rule in a @media (prefers-reduced-motion: no-preference) query so visitors who have asked their device for less motion still get an instant jump.

Reserve space for images

Give images above the targets width and height attributes so they do not push the target down after the jump. The layout shift guide covers this in more detail.

Scroll after content appears

Where a section must be built by a script, scroll to it once it exists:

function scrollToHash() {
  const el = location.hash && document.getElementById(decodeURIComponent(location.hash.slice(1)));
  if (el) el.scrollIntoView();
}
// Call after your script has rendered the content
renderSections().then(scrollToHash);

Platform notes

Single-page apps raise related crawl questions; see JavaScript rendering for crawlers.

A worked example

A long service guide has a table of contents with eight links. Readers report that Getting started moves the page, but the heading is nowhere to be seen, and Prices does nothing at all.

The console script reports one missing target: the link is #Prices and the heading is id="prices". Changing the link to lower case fixes it. The first link does jump, but the heading sits behind the 80-pixel sticky header. Adding h2[id] { scroll-margin-top: 80px; } to the theme CSS, with 64 pixels inside a phone-width media query, makes every jump land with the heading in view. A final check with JavaScript disabled shows all eight links work, and with it enabled the theme's smooth scroll still works too.

The fix, step by step

Step 1

Test with JavaScript turned off

Open the page, open DevTools, press Ctrl+Shift+P or Cmd+Shift+P, run Disable JavaScript and reload. Click each jump link. If they all work, a script is catching the clicks, so go to step six. If they still fail, the cause is in the HTML or CSS and the next steps will find it. Remember to turn JavaScript back on in the same menu before you continue.

Step 2

Match every link to an id

Paste the link-checking script into the console and read the warnings. For each missing target, compare the text after # with the target's id letter by letter, including capitals, hyphens and any encoded spaces. Change one of them so they match exactly, using lower-case words joined by hyphens. Reload and rerun the script until it prints no missing targets for links on the page.

Step 3

Remove duplicate ids

Run the duplicate-id script and look at each repeated value. Rename copies created by reused blocks or templates so every id on the page is unique, and update any link that pointed at the copy you renamed. In page builders, check saved sections and global widgets, which often carry a fixed id. Rerun the script and confirm the table it prints is empty.

Step 4

Replace name anchors with ids

Search the page source for name= on a elements used as targets, a pattern from older pages and imported content. Move the value to an id on the heading or section itself and delete the empty anchor. Then search the head for a base element; if one is present, either remove it or write same-page links in full so they resolve to the current page rather than the base address.

Step 5

Allow for the sticky header

If jumps land with the heading hidden, measure the fixed header's height in DevTools and add scroll-margin-top with that value to your targets, or scroll-padding-top on html. Use a smaller value in a phone-width media query if the header shrinks. Reload, click each link on desktop and on a phone-sized viewport, and confirm the heading appears just below the header.

Step 6

Fix scripts and routers

With JavaScript back on, search your theme and plugin scripts for click handlers on links starting with # that call preventDefault. Remove smooth-scroll plugins and use the native CSS smooth scrolling property instead, or update the script so it still finds and scrolls to the target. In React Router or similar, add a scroll-to-hash effect on route changes. Click each link again to confirm.

Step 7

Handle late and hidden content

If a target is created by JavaScript or sits inside a closed accordion, either render it in the server HTML or call a scroll function once it exists, opening its panel first. Add width and height to images above the targets so they do not move the target after the jump. Test by loading the full address with the fragment in a fresh tab, not only by clicking.

Step 8

Check links from other pages

Open each important fragment address directly, such as /pricing#plans, from another page and from a new tab. Confirm it lands on the right section with the heading visible. If a redirect sits in front of the page, check that the final address still carries the fragment. Note the ids other pages and sites link to, and keep them stable from now on.

Edge cases worth knowing

Keeping jump links working

  1. Set ids explicitly rather than letting plugins generate them from heading text, so editing a heading does not silently change its id.
  2. Run the console check on long pages after edits, and include anchor links in your broken internal link checks.
  3. Keep the scroll-margin rule in the main stylesheet so new templates inherit it.
  4. When you rename a section, keep the old id on a nearby element for a while if other sites link to it.

For the wider picture of how links between your pages should be set up, see the internal links guide.

Questions and answers

1. Why is my anchor link not jumping to the section?

An anchor link usually fails because the text after the hash does not exactly match an id on the page. Ids are case-sensitive, so a link to Pricing with a capital letter will not find an element with the id pricing. Other common causes are two elements sharing the same id, a script or router catching the click, and the target being created by JavaScript after the page loads. Testing with JavaScript turned off quickly tells you whether the cause is a script or the HTML.

2. How do I stop the heading hiding under my sticky header?

Add the CSS property scroll-margin-top to the target elements, set to the height of your sticky header, or set scroll-padding-top on the html element. The browser then stops scrolling that distance short of the top, so the heading sits just below the header. All current browsers support both properties. If your header is shorter on phones, use a media query to set a smaller value for narrow screens, and test on both sizes after the change.

3. Should I use id or name for anchor links?

You should use an id on the element you want to jump to. The older method, an empty a element with a name attribute, is obsolete in current HTML. Browsers still follow it as a fallback when no id matches, but it adds empty markup and cannot be used on other elements. Putting the id on the heading or section itself is simpler, works in every browser and gives screen readers and table of contents scripts a clear target.

4. How do I link to a section on another page?

To link to a section on another page, add the fragment to the end of that page's address, such as the pricing page path followed by a hash and plans. The browser loads the page, then scrolls to the element with that id. On the same page, the hash and name on its own is enough. Make sure the target page has the id in its server HTML, and that any redirect in front of it does not add its own fragment.

5. Why do anchor links not work in my React app?

Anchor links often fail in React apps because client-side routers handle navigation themselves and do not scroll to a fragment by default. The target may also not exist yet when the route changes, because it is rendered after data loads. Add an effect that runs when the location changes, finds the element by the fragment and scrolls to it once the content has rendered. Where possible, render the sections on the server so the browser can find them straight away.

6. Do anchor links help SEO?

Anchor links can help indirectly by making long pages easier to use, and Google sometimes shows links to sections of a page in search results when the page has clear headings with ids and a table of contents linking to them. Google does not index fragment addresses as separate pages, so a jump link does not create a new page in search. The main benefit is for readers, who find what they need without scrolling, which suits long guides, pricing pages and FAQs.

7. Can I make anchor links scroll smoothly without JavaScript?

Yes, you can make anchor links scroll smoothly with one CSS rule: set the smooth scrolling property to smooth on the html element. Wrap it in a media query for prefers-reduced-motion set to no-preference so visitors who have asked their device for less motion still get an instant jump. This replaces smooth-scroll plugins, which are a common cause of broken jump links, and it works in all current browsers. Combine it with scroll-margin-top if you have a sticky header.

Find broken links and missing targets

The internal links audit checks every link on your pages, including links to sections, and reports targets that are missing, duplicated or redirected so you can fix them in one pass.

Open Internal Link Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Internal Link Fixes  ·  Duplicate IDs  ·  Broken Internal Links  ·  Internal Links Guide  ·  Keyboard Navigation  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.