Skip to content

About implement single HTML page with multiple tabs via CSS with or without JavaScript aid

I am interested in building a web page with one single HTML file and multiple tabs.

Since only one tab at a time can be selected, I thought that the most appropriate way to handle the user’s choice of which tab to show is via radio buttons, i.e. with a <nav> wrapping a <ul> wrapping a list of <li>s each one wrapping an <input> followed by a <label> (see also my previous question).

After searching on the web and on StackOverflow, I’ve come up with this:

function f(t) {
  // the vector below should be obtainable programmatically from the HTML
  ['tab1', 'tab2'].map(x => document.getElementById(x).style.setProperty('display', 'none'));
  document.getElementById(t).style.setProperty('display', 'initial');
}
ul {
  list-style-type: none;
}

/* With this ruleset I effectively target an HTML element,
   `label`, based on a condition being true or false about
   a different HTML element, `input`. The only relation
   between them is that they are siblings. */
input:checked + label {
  color: red;
}

/* I can't do the same with the element `#tab1` because it
   is not child nor sibling of the one holding the condition,
   the `input` targeted above. So I have to do this */
.tabs {
  display: none;
}
#tab2 {
  display: initial;
}
<nav>
  <ul class="site-nav">
    <li>
      <input id="tab1-radio" value="tab1" type="radio" name="nav-tabs" onclick="f(value)">
      <label for="tab1-radio" class="box-shadow">Tab 1</label>
    </li>
    <li>
      <input id="tab2-radio" value="tab2" type="radio" name="nav-tabs" onclick="f(value)" checked="checked">
      <label for="tab2-radio" class="box-shadow">Tab 2</label>
    </li>
  </ul>
</nav>
<div id="tab1" class="tabs">
Tab 1 content
</div>
<div id="tab2" class="tabs">
Tab 2 content
</div>

However I see a few weak (or discussion) points:

  • It uses Javascript. Can’t the target design really be reached without JavaScript?
  • I have to manually match up the values of each <input> with the corresponding ids of the <div> (or <section> or whatever tag I use to wrap stuff in).
  • I have to manually attach the declaration display: initial; to the id that is that is equal to the value of the <input> that has checked="checked", which is a mouthful to say the least.

On the other hand, another answer suggests a way to accomplish the task wihtout any JavaScript. I’ve also found a more thorough guide here, but this technique relies on manual positioning and handling the stack via z-index, but that starts to seem difficult, cumbersom, and that it puts to much burden on the programmer.

Below is my attempt to simplify that approach, but it fails as described in the linked article

not displaying any tab content if you link to the page without a hash selector, i.e. you link to mypage.html rather than mypage.html#tab1.

ul {
  list-style-type: none;
}

.page {
  display: none;
}

.page:target {
  display: initial;
}
<nav>
  <ul class="site-nav">
    <li>Click <a href="#one">here</a> for page 1</li>
    <li>Click <a href="#two">here</a> for page 2</li>
  </ul>
</nav>

  <div class="page" id="one">
    Tab 1 content
  </div>

  <div class="page" id="two">
Tab 2 content
  </div>

Answer

I do not recommend this for production and I would instead encourage you to use a proper tabs pattern so you can manipulate the relevant WAI-ARIA attributes or a SPA pattern (where you would manage focus after navigation occurs) etc.

But with that being said I did want to show that this is indeed possible (having initial content) using the :target selector, provided you don’t mind having the DOM order be a little strange (which shouldn’t have any accessibility issues as the other sections are hidden).

There is no actual need for the “Home” link I have added, I just put that there for completeness / to give you options.

Also notice something unusual – because of the page change in this manner there isn’t always a <h1> – I am not actually sure (I will have to think) how to handle this best, but for now I have added a <h1> to each section so that every “page” has a <h1>.

ul {
  list-style-type: none;
}

.page{
   display: none;
} 

.page:target {
  display: initial;
}

.page:target ~ .initial  {
  display: none;
}
<nav>
  <ul class="site-nav">
   <li><a href="#home">home</a></li>
    <li><a href="#one">page 1</a> </li>
    <li><a href="#two">page 2</a> </li>
  </ul>
</nav>
<main>
  <section class="page" id="one" aria-labelledby="section1Heading">
    <h1 id="section1Heading">Tab 1 content</h1>
  </section>

   <section class="page" id="two" aria-labelledby="section2Heading">
    <h1 id="section2Heading">Tab 2 content</h1>
  </section>
  
  <section class="initial" id="home" aria-labelledby="homeHeading">
    <h1 id="homeHeading">Initial Page Content / Home Page</h1>
  </section>
  </main>