Anchor (jump) links not working: how to fix them
An anchor link, also called a jump link, takes the reader to a section of a page, such as a table of contents entry that jumps to Pricing. When it works, nobody notices. When it fails, the page does nothing, jumps to the wrong place or lands with the heading hidden under the site header, and the reader gives up scrolling. The cause is almost always one of a short list: an id that does not match, a duplicate id, a fixed header covering the target, content that does not exist yet when the browser looks for it, or a script catching the click. This guide shows you how to test for each cause in a few minutes, the markup and CSS that fix them, and the settings to check in WordPress, Shopify, React and Next.js.
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.
#, 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
| Symptom | Likely cause | Quick check |
|---|---|---|
| Nothing happens on click | No element with that id, or a case or spelling mismatch | document.getElementById('name') returns null |
| Jumps to the wrong section | Duplicate id earlier on the page | Count elements with that id |
| Jumps, but the heading is hidden | Fixed or sticky header covers the target | Scroll up slightly and the heading is there |
| Works on reload, not on first visit | Target built by JavaScript after load | View source does not contain the id |
| Goes to the home page instead | A <base href> tag changes what #name resolves to | Search the head for <base |
| Works with JavaScript off only | Smooth-scroll script, slider, tabs or router catching the click | Disable JavaScript and retry |
| Lands short or overshoots | Images above the target load late and push it down | Images lack width and height |
| Target inside a closed accordion | The element is hidden, so there is nothing to scroll to | Open 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
- WordPress. Select a heading block, open Advanced in the sidebar and fill in HTML anchor. WordPress uses that as the id. Some table of contents plugins create their own ids from heading text, so if you also set an anchor by hand, check which one appears in the page. See HTML fixes for WordPress.
- Elementor and other builders. Use the CSS ID field under the widget's Advanced tab, without the
#. Builder sticky headers usually need the scroll-margin rule above added to custom CSS. - Shopify. The rich text editor has no id field. Use the Show HTML button in the editor to add the id to the heading, or add it in the section's Liquid template. Some themes animate anchor links with their own script.
- React with React Router. Client-side routing does not scroll to a fragment by default. Add an effect that runs when the location changes and calls the scroll function above.
- Next.js. The
Linkcomponent handles links to a fragment, but a target rendered only on the client after data loads may not exist at that moment. Render the section on the server where you can.
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
- Links to another page. Use the path and fragment together, such as
/pricing#plans. On the same page,#plansalone is enough. Check that a redirect on the target page does not drop the fragment by adding its own. - Collapsed content. A target inside a closed accordion or tab cannot be scrolled to. Recent Chromium browsers open a native
<details>element when a link targets something inside it, but do not rely on that everywhere; a small script that opens the panel matching the fragment is safer. - Same link clicked twice. Scripts that store the last hash can ignore a second click. Native links do not have this problem.
- Keyboard focus. In current browsers, following a jump link also moves the starting point for the Tab key to the target, so the next press continues from that section. If focus goes back to the top instead, a script is probably intercepting the link. See keyboard navigation.
- Empty hash links.
href="#"used as a button jumps to the top of the page. Use a real<button>instead.
Keeping jump links working
- Set ids explicitly rather than letting plugins generate them from heading text, so editing a heading does not silently change its id.
- Run the console check on long pages after edits, and include anchor links in your broken internal link checks.
- Keep the scroll-margin rule in the main stylesheet so new templates inherit it.
- 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.
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.
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 โ