100% Pure HTML/CSS Page Navigation - No JavaScript Required

hakash profile image Morten Olsrud ・3 min read

Build a page navigation system using only HTML and CSS

This is a little trick I've acquired over the years, and for most use cases it works wonders in keeping my code clean, lean and free from unnecessary JavaScript.

Imagine a Single Page Application or a page showing and hiding panels, modals etc. without reloading the page. Clicking on one of these triggers will alter the content of the page.

Traditionally this was done using jQuery or some home made JavaScript functions altering the styles or classes on the elements in question. Setting a class with display: none; to hide or display: block; to show them are classics, along with setting that style directly on the elements.

Well, there is no need to do it so complicated at all.


Take a look at this HTML:

<!-- Just a typical nav-element with a list of links -->
    <a href="#page1">Page 1</a>
    <a href="#page2">Page 2</a>
    <a href="#page5">Page 5</a>
    All the divs with the content you want to serve. Notice page1 sits last. 
    This is important for the CSS to work.
<div id="page2" class="page">[some content]</div>
<div id="page3" class="page">[some content]</div>
<div id="page5" class="page">[some content]</div>
<div id="page1" class="page default">[some content]</div>

Here we have the nav element with it's links, and the divs with their content. Notice the ID of the DIV is the same as the anchor link. One important thing is placing the first page, or rather the div with the content you want to be shown when no link is clicked yet, last.

And here is the important parts of the CSS:

/* This hides all pages */
.page {
    display: none;

/* This displays the first page */
.default {
    display: block;

/* This displays the page corresponding to the one you clicked on */
:target {
    display: block;

/* This hides the default page when another page is clicked */
:target ~ .default {
    display: none;

Or condensed:

.page, :target ~ .default {
    display: none;

.default, :target {
    display: block;


Now, the CodePen I've embedded below contains CSS and HTML not displayed in this example, but that's just to make it "pretty" and has nothing to do with the actual function of the nav. Just the layout. Try it out before we unveil the secret sauce, and a caveat.

Unveiling the secret

The first part of this secret is the links. The href contains only hashed identifiers like the ones you use to jump up and down a page. The hash in a URL gets picked up by CSS using the :target pseudo-selector, allowing us to use that to change the style of the element who's ID is equal to the hash in the URL.

The second part is how we use the selectors to hide the default page. Since we are placing that last, we can use the sibling selector to target all elements with the class .default that comes after the element who is targeted.

Now the caveat is that unless the page is structured or laid out in such a way that the top of the "pages" are also at the top of the browser, the page might jump. Some CSS/HTML only hacks can be done by using container DIVs that holds the ID and are positioned with top: 0px; and contains the actual content DIV pushed down with margins, or some other similar approach.

Hope you enjoyed!

Oh, and if you have other approaches to do SPA and similar type navigation without JavaScript, please share in the comments.

Posted on by:

hakash profile

Morten Olsrud


Diving into Ruby and Rails, with all the trimmings, these days. Impostor syndrome here i come!


markdown guide

Slightly simpler version, making use of the :not() and :target
This allows for the use of a non-hash link for the homepage.
And no need for the display:block

    section:target ~ #home {
        display: none
    <a href="/">home</a>
    <a href="#contact">contact</a>
    <a href="#about">about</a>
    <a href="#blog">blog</a>
    <section id="contact">contact section...</section>
    <section id="about">about section...</section>
    <section id="blog">blog section...</section>
    <section id="home">home section...</section>

Thanks for the hack! I learned a lot from it!
One question though, from some reason every time I press a href on a page (that is not the default, lets call it page2) directing to somewhere on page2, it suddenly displays the default page.
Is there a chance that when I press another link, that is not a page, the :target{ display:block;} is activated anyways?


Sorry for the late comeback.

If the other links you are using internally on the page causes the hash in the URL to change, then yes, what you are describing is correct and the expected behaviour. This trick relies on the hash in the URL, as that is what triggers the CSS :target selector.


Another big caveat is the accessibility. We should use aria-attributes, but at this point we are already using js.


Yeah, I'd love to do that, but I havent found a way for CSS selectors to work on aria. I'm also unsure how a screen-reader or braille-board would react to most SPAs to be honest.


You should probably use the attribute selector [aria-active]{} but as i said, you need js.

Regardless of JS, that selector was new to me 😊