skip to Main Content

A jump link is a path that ends with a hash, e.g. /mypage#heading. After navigating to the path portion (e.g. /mypage), the browser will:

  1. Look for an element with an ID that matches the hash (e.g. #heading would be matched by <h1 id="heading">) and if one is found will scroll (or "jump") that element into view
  2. Make the matching element have the CSS :target pseudoselector state

The first item is easy to implement with $element.scrollIntoView.

It’s also easy to change the appearance of the target element by using JS to toggle CSS classes and HTML attributes.

But I can’t figure out how to use JavaScript to make an element have the :target pseudoselector state. For accessibility reasons (and for my own curiosity) I want to reproduce the browser’s native jump link behavior, not something that just looks like it.

Manually dispatching HashChangeEvent('hashchange') doesn’t appear to have the desired effect.

Note when you run the demo code below: if you click a jump link for an element on the current "page" then that element is correctly highlighted, but if you click a jump link for an element on a different page then it is not highlighted, indicating :target is not set.

How can JavaScript make an element have the CSS :target pseudoselector state?

for (const $anchor of document.querySelectorAll(`a`)) {
  $anchor.innerHTML = $anchor.getAttribute(`href`);
  $anchor.addEventListener(`click`, onClick);
}

render(`/js`); // This is the path StackOverflow uses to run demo code snippets

function onClick(event) {
  const [path, hash] = event.target.getAttribute(`href`).split(`#`);

  if (path === location.pathname) {
    return;
  }
  
  event.preventDefault();
  window.history.pushState({}, ``, path);
  
  render(path);

  if (hash) {
    const $target = document.getElementById(hash);
    if ($target) {
      $target.scrollIntoView();
    }
    window.dispatchEvent(new HashChangeEvent(`hashchange`));
  }
}

function render(pageName) {
  const page = document.getElementById(pageName).innerHTML;
  document.getElementById(`output`).innerHTML = page;
}
#output {
  height: 200px;
  outline: 1px solid #00000060;
  width: 300px;
}

*:target {
  background: gold;
}
<template id="/js">
  <h1 id="target1">Target 1</h1>
  <h1 id="target2">Target 2</h1>
</template>

<template id="/other">
  <h1 id="target3">Target 3</h1>
  <h1 id="target4">Target 4</h1>
</template>


<ul>
  <li><a href="/js#target1" target="_self"></a></li>
  <li><a href="/js#target2" target="_self"></a></li>
  <li><a href="/other#target3" target="_self"></a></li>
  <li><a href="/other#target4" target="_self"></a></li>
</ul>

<p>Current "page":</p>

<div id="output"></div>

2

Answers


  1. I don’t think you can do that. But for your problem, a simple solution can solve it without modify :target.

    Since you let browser do native page jump if you have same pathname. I suggest you re-click the link when you rendered new page instead of using HashChangeEvent(hashchange)

    This is how I do it, hope it’ll help

    for (const $anchor of document.querySelectorAll(`a`)) {
      $anchor.innerHTML = $anchor.getAttribute(`href`);
      $anchor.addEventListener(`click`, onClick);
    }
    
    render(`/js`); // This is the path StackOverflow uses to run demo code snippets
    
    function onClick(event) {
      const [path, hash] = event.target.getAttribute(`href`).split(`#`);
    
      if (path === location.pathname) {
        return;
      }
      
      event.preventDefault();
      window.history.pushState({}, ``, path);
      
      render(path);
    
      if (hash) {
        //const $target = document.getElementById(hash);
        //if ($target) {
        //  $target.scrollIntoView();
        //}
        //window.dispatchEvent(new HashChangeEvent(`hashchange`));
        event.target.click();
      }
    }
    
    function render(pageName) {
      const page = document.getElementById(pageName).innerHTML;
      document.getElementById(`output`).innerHTML = page;
    }
    #output {
      height: 200px;
      outline: 1px solid #00000060;
      width: 300px;
    }
    
    *:target {
      background: gold;
    }
    <template id="/js">
      <h1 id="target1">Target 1</h1>
      <h1 id="target2">Target 2</h1>
    </template>
    
    <template id="/other">
      <h1 id="target3">Target 3</h1>
      <h1 id="target4">Target 4</h1>
    </template>
    
    
    <ul>
      <li><a href="/js#target1" target="_self"></a></li>
      <li><a href="/js#target2" target="_self"></a></li>
      <li><a href="/other#target3" target="_self"></a></li>
      <li><a href="/other#target4" target="_self"></a></li>
    </ul>
    
    <p>Current "page":</p>
    
    <div id="output"></div>
    Login or Signup to reply.
  2. the CSS :target pseudo selector will simply grab whatever the current URL hash is and if that hash is a valid html id present on the page, it will mark that element as "target". it doesn’t matter how its added, be it by clicking a link with hash, or a hash directly added on the link when opening, or hash updated logically via JS, it simply listen for url hash,

    the reason it directly works without javascript is simply because if you click on a link with hash in it e.i. <a href="#t1">Test 1</a> the hash will be appended on the URL by default.

    Here’s a simple example,

    • it programmatically append hash on the URL using javascript.
    • it will also work if you remove the javascript because its a default behavior’s of hash link
    • if you comment the link window.location.hash = target.getAttribute("href") it will not work because the hash is not appended on the URL
    const targets = document.querySelectorAll(".target a")
    targets.forEach(function(target) {
        target.addEventListener('click', event => {
            event.preventDefault()
            window.location.hash = target.getAttribute("href")
        })
    })
    p:target {
        background: #222;
        color: #fff;
    }
    p:target::before {
        content: "🥴";
        color: blue;
        margin-right: 0.25em;
    }
    <ol class="target">
        <li><a href="#t1">Test 1</a></li>
        <li><a href="#t2">Test 2</a></li>
    </ol>
    <p id="t1">Test 1 Content</p>
    <p id="t2">Test 2 Content</p>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search