As best I'm aware, there are two ways to change the hash at the end of the URL. window.location.hash and history.pushState. However, history.pushState does not trigger the CSS :target pseudo-class so that's out.
An answer to "Modifying document.location.hash without page scrolling" demonstrates a method to modify location.hash without scrolling, but this workaround fails to trigger the :target pseudo-class on the element with a matching ID.
Below is a simple example. Two links work as expected: :target is triggered and the tabs display, but they'll also scroll into view if necessary. Two links don't work: :target isn't triggered, but scrolling is prevented.
(function() {
  function setHash(event) {
    event.preventDefault();
    var decoy = document.querySelector(".dummy-tabpanel");
    var tabId = event.currentTarget.getAttribute('href');
    var id = tabId.replace( /^#/, '' );
    var tabPanel = document.getElementById(id);
    
    decoy.style.top = document.body.scrollTop + 'px';
    
    tabPanel.setAttribute('id', '');
    decoy.setAttribute('id', id);
    
    window.location.hash = tabId;
    
    decoy.setAttribute('id', '');
    tabPanel.setAttribute('id', id);
    
    return false;
  }
  
  function setHashDirectly(event) {
    event.preventDefault();
    var tabId = event.currentTarget.getAttribute('href');
    window.location.hash = tabId;
    return false;
  }
  
  var tabLinks = document.querySelectorAll('a[href^="#"]');
  
  for (var tabLink of tabLinks) {
    tabLink.addEventListener("click", tabLink.classList.contains('direct') ? setHashDirectly : setHash );
  }
})().tabs {
  margin-top: 300px; // attempt to make scrolling necessary
}
.tabs [role="tabpanel"] {
  display: none;
}
.tabs [role="tabpanel"]:target {
  display: block;
}
.dummy-tabpanel {
  position: absolute;
  visibility: hidden;
}<nav>
  <ul>
    <li>
      <a href="#one">one (doesn't scroll or trigger <code>:target</code>)</a>
    </li>
    <li>
      <a href="#two">two (doesn't scroll or trigger <code>:target</code>)</a>
    </li>
    <li>
      <a href="#three" class="direct">three (triggers <code>:target</code> but scrolls)</a>
    </li>
    <li>
      <a href="#four" class="direct">four (triggers <code>:target</code> but scrolls)</a>
    </li>
  </ul>
</nav>
<div class="tabs">
  <div role="tabpanel" id="one">
    this is the first tab (won't display)
  </div>
  <div role="tabpanel" id="two">
    this is the two tab (won't display)
  </div>
  <div role="tabpanel" id="three">
    this is the third tab (should display)
  </div>
  <div role="tabpanel" id="four">
    this is the forth tab (should display)
  </div>
</div>
<div class="dummy-tabpanel"></div>Is there some way to get the "best of both worlds," i.e. change the window hash without scrolling the page and have CSS :target triggered?
Note: I've tested this on Chrome 64 and Firefox 58 on OS X 10.13.
 
    