'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 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 too 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>


Solution 1:[1]

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>

Solution 2:[2]

I think it could help you.

.content1,
.content2,
.content3 {
    display: none;
    padding: 20px;
    border-top: 2px solid #999999;
}

input[type='radio'] {
    width: 0;
    height: 0;
    opacity: 0;
}

label {
    cursor: pointer;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    width: 80px;
    height: 30px;
    background-color: #dddddd;
    border-style: solid solid none solid;
    border-width: 2px;
    border-color: transparent;
}

#tab1:checked+label {
    border-color: #999999;
}

#tab2:checked+label {
    border-color: #999999;
}

#tab3:checked+label {
    border-color: #999999;
}

#tab1:checked~.content1 {
    display: block;
}

#tab2:checked~.content2 {
    display: block;
}

#tab3:checked~.content3 {
    display: block;
}
<div class="container">
    <input type="radio" name="tab" id="tab1" checked>
    <label for="tab1">content1</label>
    <input type="radio" name="tab" id="tab2">
    <label for="tab2">content2</label>
    <input type="radio" name="tab" id="tab3">
    <label for="tab3">content3</label>
    <div class="content1" id="content1">This is first content</div>
    <div class="content2" id="content2">This is second content</div>
    <div class="content3" id="content3">This is third content</div>
</div>

Solution 3:[3]

You could write code like this, but it won't work because Selector4 hasn't been introduced yet.

.content1,
.content2,
.content3 {
    display: none;
    padding: 20px;
    border-top: 2px solid #999999;
}

input[type='radio'] {
    width: 0;
    height: 0;
    opacity: 0;
}

label {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    width: 80px;
    height: 30px;
    background-color: #dddddd;
    border-style: solid solid none solid;
    border-width: 2px;
    border-color: transparent;
}

#tab1:checked+label {
    border-color: #999999;
}

#tab2:checked+label {
    border-color: #999999;
}

#tab3:checked+label {
    border-color: #999999;
}

div:has(#tab1:checked)~div .content1 {
    display: block;
}

div:has(#tab2:checked)~div .content2 {
    display: block;
}

div:has(#tab3:checked)~div .content3 {
    display: block;
}
<div class="container">
    <div class="tab-container">
        <input type="radio" name="tab" id="tab1" checked><label for="tab1">content1</label>
        <input type="radio" name="tab" id="tab2"><label for="tab2">content2</label>
        <input type="radio" name="tab" id="tab3"><label for="tab3">content3</label>
    </div>
    <div class="content-container">
        <div class="content1" id="content1">This is first content</div>
        <div class="content2" id="content2">This is second content</div>
        <div class="content3" id="content3">This is third content</div>
    </div>
</div>

Solution 4:[4]

As you can see in the below HTML snippet, we are using simple input radio buttons fields and labels for every tab title. Then we are adding the content for every tab in a separate div class name tab__content. Each input radio button has a unique ID, so we can then style it through the CSS.

body {
    font-family: 'cursive';
    background-color: #e7e7e7;
    color: #777;
    font-weight: 300;
}

.tab-wrap {
    -webkit-transition: 0.3s box-shadow ease;
    transition: 0.3s box-shadow ease;
    border-radius: 6px;
    max-width: 100%;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
    position: relative;
    list-style: none;
    background-color: #fff;
    margin: 40px 0;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}

.tab-wrap:hover {
    box-shadow: 0 12px 23px rgba(0, 0, 0, 0.23), 0 10px 10px rgba(0, 0, 0, 0.19);
}

.tab {
    display: none;
}

.tab:checked:nth-of-type(1)~.tab__content:nth-of-type(1) {
    opacity: 1;
    -webkit-transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease, 0.8s -webkit-transform ease;
    position: relative;
    top: 0;
    z-index: 100;
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    text-shadow: 0 0 0;
}

.tab:checked:nth-of-type(2)~.tab__content:nth-of-type(2) {
    opacity: 1;
    -webkit-transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease, 0.8s -webkit-transform ease;
    position: relative;
    top: 0;
    z-index: 100;
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    text-shadow: 0 0 0;
}

.tab:checked:nth-of-type(3)~.tab__content:nth-of-type(3) {
    opacity: 1;
    -webkit-transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease, 0.8s -webkit-transform ease;
    position: relative;
    top: 0;
    z-index: 100;
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    text-shadow: 0 0 0;
}

.tab:checked:nth-of-type(4)~.tab__content:nth-of-type(4) {
    opacity: 1;
    -webkit-transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease, 0.8s -webkit-transform ease;
    position: relative;
    top: 0;
    z-index: 100;
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    text-shadow: 0 0 0;
}

.tab:checked:nth-of-type(5)~.tab__content:nth-of-type(5) {
    opacity: 1;
    -webkit-transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s -webkit-transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease;
    transition: 0.5s opacity ease-in, 0.8s transform ease, 0.8s -webkit-transform ease;
    position: relative;
    top: 0;
    z-index: 100;
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    text-shadow: 0 0 0;
}

.tab:first-of-type:not(:last-of-type)+label {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.tab:not(:first-of-type):not(:last-of-type)+label {
    border-radius: 0;
}

.tab:last-of-type:not(:first-of-type)+label {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

.tab:checked+label {
    background-color: #fff;
    box-shadow: 0 -1px 0 #fff inset;
    cursor: default;
}

.tab:checked+label:hover {
    box-shadow: 0 -1px 0 #fff inset;
    background-color: #fff;
}

.tab+label {
    box-shadow: 0 -1px 0 #eee inset;
    border-radius: 6px 6px 0 0;
    cursor: pointer;
    display: block;
    text-decoration: none;
    color: #333;
    -webkit-box-flex: 3;
    -ms-flex-positive: 3;
    flex-grow: 3;
    text-align: center;
    background-color: #f2f2f2;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    text-align: center;
    -webkit-transition: 0.3s background-color ease, 0.3s box-shadow ease;
    transition: 0.3s background-color ease, 0.3s box-shadow ease;
    height: 50px;
    box-sizing: border-box;
    padding: 15px;
}

.tab+label:hover {
    background-color: #f9f9f9;
    box-shadow: 0 1px 0 #f4f4f4 inset;
}

.tab__content {
    padding: 10px 25px;
    background-color: transparent;
    position: absolute;
    width: 100%;
    z-index: -1;
    opacity: 0;
    left: 0;
    -webkit-transform: translateY(-3px);
    transform: translateY(-3px);
    border-radius: 6px;
}
<div class="tab-wrap">
   <!-- active tab on page load gets checked attribute -->
   <input type="radio" id="tab1" name="tabGroup1" class="tab" checked>
   <label for="tab1">Short</label>
   <input type="radio" id="tab2" name="tabGroup1" class="tab">
   <label for="tab2">Medium</label>
   <input type="radio" id="tab3" name="tabGroup1" class="tab">
   <label for="tab3">Long</label>
   <div class="tab__content">
      <h3>Short Section</h3>
      <p>Praesent nonummy mi in odio. Nullam accumsan lorem in dui. Vestibulum turpis sem, aliquet eget, lobortis pellentesque, rutrum eu, nisl. Nullam accumsan lorem in dui. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.</p>
   </div>
   <div class="tab__content">
      <h3>Medium Section</h3>
      <p>Praesent nonummy mi in odio. Nullam accumsan lorem in dui. Vestibulum turpis sem, aliquet eget, lobortis pellentesque, rutrum eu, nisl. Nullam accumsan lorem in dui. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.</p>
      <p>In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Morbi mattis ullamcorper velit. Pellentesque posuere. Etiam ut purus mattis mauris sodales aliquam. Praesent nec nisl a purus blandit viverra.</p>
   </div>
   <div class="tab__content">
      <h3>Long Section</h3>
      <p>Praesent nonummy mi in odio. Nullam accumsan lorem in dui. Vestibulum turpis sem, aliquet eget, lobortis pellentesque, rutrum eu, nisl. Nullam accumsan lorem in dui. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.</p>
      <p>In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Morbi mattis ullamcorper velit. Pellentesque posuere. Etiam ut purus mattis mauris sodales aliquam. Praesent nec nisl a purus blandit viverra.</p>
      <p>Praesent nonummy mi in odio. Nullam accumsan lorem in dui. Vestibulum turpis sem, aliquet eget, lobortis pellentesque, rutrum eu, nisl. Nullam accumsan lorem in dui. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.</p>
      <p>In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Morbi mattis ullamcorper velit. Pellentesque posuere. Etiam ut purus mattis mauris sodales aliquam. Praesent nec nisl a purus blandit viverra.</p>
   </div>
</div>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Enlico
Solution 2 Enlico
Solution 3 Sato Takeru
Solution 4 Dip Vachhani