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.