sameold
sameold

Reputation: 19242

Is it possible to have tabs without javascript

I'm trying to have a box with tabs, and have found many tutorials on how it's done with javascript to switch between the tabs. Is there anyway to have tabs without javascript?

Upvotes: 21

Views: 52458

Answers (5)

David
David

Reputation: 305

perfect layout with :has()

(But unsemantic and boilerplate-y)

<div class='tabs'>

    <div class='wrapper'>
        <div class='bar'>
            <input class='tab' id='tabs_0_tab_1'
                type='radio'
                name='tabs_0'
                checked
            />
            <label class='tab' for='tabs_0_tab_0'>Tab 1</label>
            <label class='tab' for='tabs_0_tab_1'>Second Tab</label>
        </div>
        <p>Lorem...</p>
        <p>Ipsum...</p>
    </div>

    <div class='wrapper'>
        <div class='bar'>
            <label class='tab' for='tabs_0_tab_0'>Tab 1</label>
            <input class='tab' id='tabs_0_tab_1'
                type='radio'
                name='tabs_0'
            />
            <label class='tab' for='tabs_0_tab_1'>Second Tab</label>
        </div>
        <p>Ipsum...</p>
        <p>Lorem...</p>
    </div>

</div>
.tabs > .wrapper {

    & > .bar {
        display: flex;
        gap: 1rem;

        & > input.tab {
            display: none;
        }
    }

    &:not(:has( > .bar > .tab:checked )) {
        display: none;
    }
}

.wrapper:has( > .bar > input:not(:checked) ) {
    display: none;
}

Effectively, the bar and the content can be styled as if there’s nothing fancy going on. As a simple example:

.tabs > .wrapper > .bar {
    border-bottom: 2px solid #99999966;

    & > .tab {
        --line-width: 2px;
        border-block: solid transparent var(--line-width);
        margin-bottom: calc( var(--line-width) - 2px );
        padding-block: .25em;
        cursor: pointer;

        &:is(:checked) + .tab {
            border-bottom-color: #888888;
        }
    }
}

This pattern has some disadvantages: it’s not particularly semantic; it requires the new :has() selector; and it’s rather boilerplate-y, so using it without a language to automate the repetitive parts could be a chore.

Upvotes: 0

krispy
krispy

Reputation: 1314

EDIT: As of 2020 this technique still works and allows tabs to be linked to, but if you are looking for multiple tabsets on one page, see @chulian's answer which uses input[type=radio] instead of :target.

There is an archive of the now-dead html5rockstars.com demo here: https://web.archive.org/web/20121118201357/http://htmlrockstars.com/blog/using-css-to-create-a-tabbed-content-area-no-js-required/

The same technique is covered, arguably better, here: http://www.sitepoint.com/css3-tabs-using-target-selector/

What it boils down to is that you use the CSS3 :target selector to unhide whichever tab is currently selected. This will only work if there is only one set of tabs on the page, but has the advantage of complete browser back button support. For example:

<ul id="menu">
    <li><a href="#tab1">First tab</a></li>
    <li><a href="#tab2">Second tab</a></li>
    <li><a href="#tab3">Third tab</a></li>
</ul>
<div id="tab1" class="tab-content">Content of first tab</div>
<div id="tab2" class="tab-content">Content of second tab</div>
<div id="tab3" class="tab-content">Content of third tab</div>

And then in your stylesheet:

.tab-content {
    display: none;
}
.tab-content:target {
    display: block;
}

Unfortunately this isn't perfect, as no tab content will show up until one of the links is clicked (unless you link to page.html#tab1). The second link above suggests something like the following as a solution to that issue:

.tab-content {
    z-index: 0;
    background-color: white;
    position: absolute;
    top: 100px;
    width: 100%;
    height: 300px;
}
.tab-content:first-child {
    z-index: 1;
}
.tab-content:target {
    z-index: 2;
}

This is somewhat hackish, and also requires absolute positioning.

As an alternative, if you do not mind having your default tab be last in the html (you can order the links however you like, of course), you could do this:

<ul id="menu">
    <li><a href="#tab1">First tab</a></li>
    <li><a href="#tab2">Second tab</a></li>
    <li><a href="#tab3">Third tab</a></li>
</ul>
    <div class="tab-folder">
    <div id="tab2" class="tab-content">Content of second tab</div>
    <div id="tab3" class="tab-content">Content of third tab</div>
    <div id="tab1" class="tab-content">Content of first tab</div>
</div>

CSS:

.tab-folder > .tab-content:target ~ .tab-content:last-child, .tab-folder > .tab-content {
    display: none;
}
.tab-folder > :last-child, .tab-folder > .tab-content:target {
    display: block;
}

This is arguably the cleanest solution and the one that I would choose over the others, unless I suspected that many people would be visiting my page with CSS turned off.

Upvotes: 24

David
David

Reputation: 305

The HTML5 <details> element semantically means "more content here". With its show-hide function, reflected attribute, and coordinating name attribute, it's almost a perfect fit for implementing tabs without JavaScript. Almost.

perfect semantics, poor layout

<div class='tabs'>

    <details class='tab' name='these_tabs' open>
        <summary class='tab-title'>Tab One</summary>
        <p>Lorem...</p>
        <p>Ipsum...</p>
    </details>

    <details class='tab' name='these_tabs'>
        <summary class='tab-title'>Tab One</summary>
        <p>Lorem...</p>
        <p>Ipsum...</p>
    </details>

</div>

This is semantic. Unfortunately, even with subgrid now widely adopted, this limits the tab and its content to using the same width. Either the sibling tabs are pushed off to the side so the active tab's content has enough room, or the active tab's content is crammed into as narrow a space as possible.

So could the content be moved outside the details element?

partly semantic, good layout

<div class='tabs'>

    <details class='tab' name='these_tabs' open>
        <summary class='title'>Tab one</summary>
    </details>
    <div class='content'>
        <p>Lorem...</p>
        <p>Ipsum...</p>
    </div>

    <details class='tab' name='these_tabs' open>
        <summary class='title'>Tab one</summary>
    </details>
    <div class='content'>
        <p>Lorem...</p>
        <p>Ipsum...</p>
    </div>

</div>
.tabs {
    display: grid;
    grid-auto-flow: column;
    grid-template-rows: auto auto;
}
.tab {
    grid-row: 1;
    max-width: max-content;
}
.tab-content {
    grid-column: 1 / 99; /* [1] */
    grid-row: 2;
}
.tab[open] .tab-title {
    pointer-events: none; /* [2] */
}
.tab:not([open]) + .tab-content { /* [3] */
    display: none;
}

This is less semantic, because the content to-be-revealed is not actually inside the <details> element, so a screen reader would have to infer its connection from proximity and visibility. On the other hand, it does use the appropriate element for interaction. It also achieves flexible horizontal layout of the tabs where the active tab can be selected and styled. And finally, the content is not removed from the flow, so it consumes as much space as it needs, and no more.

CSS notes

The tab-switching trick is much the same as the canonical input:checked + * method. The essential layout trick with grid is to force all the tabs onto the top row, then assign the bar's remaining width to an empty column for the content to span across.

[1] Unfortunately, while styling this with grid-column: 1 / -1 ("span the first grid-line to the last grid-line") should theoretically work, it does not. Implicitly-created columns are not included in the calculation, so it only spans the explicitly created columns (a grand total of 1, in this case). Spanning 1 more grid-column than you have tabs is the minimum to make this work.

[2] Disabling pointer-events on the <summary> element of the open <dialog> forces a user to keep one tab open at all times.

[3] By using the :not() selector, the tab CSS leaves me free to set whatever display property I want on the tab content.

Upvotes: 0

This method is slightly different from the :target method, using overflow and named anchors instead. The benefit is that your CSS doesn't need to know anything about dynamic content. Generated markup only needs to match

<a href="#myFirstTab">Tab A</a> in the navigation with

<a name="myFirstTab"></a> inside the tab itself (at the end, preferably)

.ContentArea{ 
  width: 50vw;
  height: 50vh;
  position: absolute;
  z-index: 1;
  top: 25vh;
  left: 25vw;
  overflow: hidden;
 }
.tab{
  height: 100%;
  width: 100%;
  overflow-x: hidden;
  overflow-y: auto;
  position: relative;
  z-index: 2;
}
.clearance{
  
  position: absolute;
  top: 0px;
  left: 0px;
  right: 0px;
  bottom: 0px;
}
<html>
<head>
<title>Tab Sandbox</title>
<body>

  <nav class="ribbon">
    <a href="#tab1">A</a> | 
    <a href="#tab2">B</a> | 
    <a href="#tab3">C</a> | 
    <a href="#tab4">D</a> | 
    <a href="#tab5">E</a>
    <a href="#tab6">F</a>
  </nav>
  <div class="ContentArea"> 
    
    <div class="tab">
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa <br/>
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa <br/>
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa <br/>
      <div class="clearance">
        <a name="tab1"></a>
      </div>
    </div>
    <div class="tab">
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb <br/>
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb <br/>
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb <br/>
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb <br/>
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb <br/>
      <div class="clearance">
        <a name="tab2"></a>
      </div>
    </div>


    <div class="tab">
    cccccccccccccccccccccccccccccc <br/>
    cccccccccccccccccccccccccccccc <br/>
    cccccccccccccccccccccccccccccc <br/>
    cccccccccccccccccccccccccccccc <br/>
      <div class="clearance">
        <a name="tab3"></a>
      </div>
    </div>


    <div class="tab">
    dddddddddddddddddddddddddddddd <br/>
    dddddddddddddddddddddddddddddd <br/>
    dddddddddddddddddddddddddddddd <br/>
    dddddddddddddddddddddddddddddd <br/>
      <div class="clearance">
        <a name="tab4"></a>
      </div>
    </div>


    <div class="tab">
    eeeeeeeeeeeeeeeeeeeeeeeeeeeeee <br/>
    eeeeeeeeeeeeeeeeeeeeeeeeeeeeee <br/>
    eeeeeeeeeeeeeeeeeeeeeeeeeeeeee <br/>
    eeeeeeeeeeeeeeeeeeeeeeeeeeeeee <br/>
      <div class="clearance">
        <a name="tab5"></a>
      </div>
    </div>


    <div class="tab">
    ffffffffffffffffffffffffffffff <br/>
    ffffffffffffffffffffffffffffff <br/>
    ffffffffffffffffffffffffffffff <br/>
    ffffffffffffffffffffffffffffff <br/>

      <div class="clearance">
        <a name="tab6"></a>
      </div>
    </div>

  </div>

</body>
</head>
</html>

This is nice, for one, because it only uses three simple CSS selectors and they are ALL classes. No strict requirements that come with IDs

The basic idea is you create a viewport with known dimensions, positioned either relatively or absolutely, somewhere on the page. This will be the display area for your tabs. Now you clip overflow to hide all but the first tab, rather than display: none

This faces a few challenges, so you may notice the "clearance" div at the end of each tab actually holds the named anchor. This div helps ensure that content is pushed around properly and not partly scrolled into view. Occasionally needs small tweaks, always very simple based on your design needs.

If you like, the nav links can be embedded in radio buttons to detect "checked" status. I've also done it where the nav is simply repeated in each tab with the active class varied. While redundant, it presents an interest navigation which can be dynamic per-tile, giving a 2D space. Fun to play with, but usually I just opt for toggling a class in the nav with jQuery.toggle()

Upvotes: 1

chulian
chulian

Reputation: 4097

Found this one, hope it solves your question

http://css-tricks.com/functional-css-tabs-revisited/

demo: http://css-tricks.com/examples/CSSTabs/radio.php

<div class="tabs">

   <div class="tab">
       <input type="radio" id="tab-1" name="tab-group-1" checked>
       <label for="tab-1">Tab One</label>

       <div class="content">
           stuff
       </div> 
   </div>

   <div class="tab">
       <input type="radio" id="tab-2" name="tab-group-1">
       <label for="tab-2">Tab Two</label>

       <div class="content">
           stuff
       </div> 
   </div>

    <div class="tab">
       <input type="radio" id="tab-3" name="tab-group-1">
       <label for="tab-3">Tab Three</label>

       <div class="content">
           stuff
       </div> 
   </div>

</div>

css

.tabs {
  position: relative;   
  min-height: 200px; /* This part sucks */
  clear: both;
  margin: 25px 0;
}
.tab {
  float: left;
}
.tab label {
  background: #eee; 
  padding: 10px; 
  border: 1px solid #ccc; 
  margin-left: -1px; 
  position: relative;
  left: 1px; 
}
.tab [type=radio] {
  display: none;   
}
.content {
  position: absolute;
  top: 28px;
  left: 0;
  background: white;
  right: 0;
  bottom: 0;
  padding: 20px;
  border: 1px solid #ccc; 
}
[type=radio]:checked ~ label {
  background: white;
  border-bottom: 1px solid white;
  z-index: 2;
}
[type=radio]:checked ~ label ~ .content {
  z-index: 1;
}

Upvotes: 17

Related Questions