0

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>
Enlico
  • 23,259
  • 6
  • 48
  • 102

4 Answers4

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>
Enlico
  • 23,259
  • 6
  • 48
  • 102
Sato Takeru
  • 1,092
  • 1
  • 5
  • 16
  • But this mixes up the tabs and their content in one single generic `div`. – Enlico Oct 10 '21 at 17:01
  • I don't think there is any way to solve your problem without JS other than this way. – Sato Takeru Oct 10 '21 at 17:04
  • 2
    Because there isn't a CSS selector to select the parent elements yet. [Parent Selector Question](https://stackoverflow.com/questions/1014861/is-there-a-css-parent-selector) – Sato Takeru Oct 10 '21 at 17:06
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>
Enlico
  • 23,259
  • 6
  • 48
  • 102
GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • Nice trick! Why do you suggest to refrain from using it? Besides, would you mind providing a link to some resources about this _proper tabs pattern_ that you mention? – Enlico Oct 11 '21 at 16:01
  • https://www.deque.com/blog/a11y-support-series-part-1-aria-tab-panel-accessibility/ did a great write up on Tabs. I wouldn't use the code there (they use jQuery as it is an older post, inline styles etc.) but all of the principles are well explained. The pattern in my answer will work, but the DOM order not matching the order of the "pages" is my biggest issue as if you ever wanted to introduce transitions between tabs it would probably break / result in workarounds, the heading level issue is another strange one I can't decide if it works or not etc. Thanks for tidying my answer by the way! – GrahamTheDev Oct 12 '21 at 05:25
  • (I'm still exploring this topic, as I dont' want to spend time coding without truly understanding the consequence of some choices.) I see that your solution (the one you don't recommend) has the benefit of appending the `href` of the cliked `a` to the URL, so that the chosen tab resists page refreshes. This seems good, but the [page you suggested in the comment](https://www.deque.com/blog/a11y-support-series-part-1-aria-tab-panel-accessibility/) doesn't guide to do anything like that. Am I misunderstanding that solution? – Enlico Oct 17 '21 at 17:48
  • Just to add to that, I see that the tabs in a GitHub profile page (e.e. [here's mine](https://github.com/Aster89?tab=repositories)) are resistent to refresh, because which tab is selected is encoded in the URL: a trailing `?tab=repositories`, for instance encodes the _Repositories_ tab. And looking at the HTML I see that those tabs are ``s in a ` – Enlico Oct 17 '21 at 17:52
  • Finally, I'm having some difficulty in finding resources on this thing of preserving the state of the page (which tab is selected, if a checkbox is selected, how much the user has scrolled down the page, ...) across refreshes. It looks like some of these things (e.g. which tab is selected) are encoded in the URL, as per my previous comment. But I don't even know the terminology of this things, so I'm stuck without knowing what to search :/ – Enlico Oct 17 '21 at 17:58
0

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>
Sato Takeru
  • 1,092
  • 1
  • 5
  • 16
0

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>