Tabset

An in-page tab set with no JavaScript and no IDs. Inside a div class="tabset", a label wrapping a radio is a tab, and the element right after it is that tab's content. The radios share a name, so only one is ever selected, and the stylesheet shows that tab's content and hides the rest.

<div class="tabset">
  <label><input type="radio" name="view" checked> Overview</label>
  <div>
    …the Overview content…
  </div>
  <label><input type="radio" name="view"> Rules</label>
  <div>
    …the Rules content…
  </div>
</div>
A tab is a label with a radio; the next element is its content

A tab set

Overview

The element straight after a tab holds its content. It drops onto its own row below the strip and takes the panel surface, so the tab and its content read as one unit.

Rules

Pick another tab and the stylesheet swaps which content shows. The radios carry the state, so it holds without a line of script.

Members

Every tab follows the same shape: a label with a radio, then the block of content it reveals.

Log

Mark one radio checked and that tab opens on load.

<div class="tabset">
  <label><input type="radio" name="tabs-basic" checked> Overview</label>
  <div>
    <h3>Overview</h3>
    <p>…</p>
  </div>
  <label><input type="radio" name="tabs-basic"> Rules</label>
  <div>
    <h3>Rules</h3>
    <p>…</p>
  </div>
</div>
Four tabs, the first open; each label is followed by its content

The content is any element

The content carries no class, so it is whatever element the content needs: a div of mixed content, a bare p, a table, a form. The stylesheet only cares that it sits right after the tab.

A single paragraph can be the whole content, with no wrapper around it.

LoginGroup
adaEditors
graceViewers
<div class="tabset">
  <label><input type="radio" name="tabs-el" checked> Summary</label>
  <p>A single paragraph can be the whole content.</p>
  <label><input type="radio" name="tabs-el"> People</label>
  <div>
    <table> … </table>
  </div>
  <label><input type="radio" name="tabs-el"> Actions</label>
  <div class="buttons"> … </div>
</div>
Three tabs whose content is a paragraph, a table and a button group

Reading the selection

The tabs are a radio group, so a script reads the chosen tab from the checked radio. Give each radio a value and listen for change.

<label><input type="radio" name="view" value="overview" checked> Overview</label>
<label><input type="radio" name="view" value="rules"> Rules</label>

<script>
  const tabs = document.querySelector('.tabs')
  tabs.addEventListener('change', e => console.log(e.target.value))
  tabs.querySelector('input:checked').value   // the current tab
</script>
Same markup, plus a value on each radio to read from script

Nested tabs

Tabs nest: put a second set inside a tab's content and it runs on its own, because the child combinator keeps each set's rules to its own direct children. The one thing to settle is grouping. Radios group by a shared name within a form, so wrap a set in a form and every set on the page, outer and inner, can reuse the one name. A form cannot nest, so the outer set stays a div and each inner set is the form. The inner sets below are vertical.

Overview

Each tab here opens a vertical set of its own, kept apart by its form.

The inner tabs stack down the left, their content beside them.

Selecting an inner tab leaves the outer tab untouched.

All of these radios are named tab, yet each set is its own group.

Rules

A separate form, so its selection is independent of the set above.

One constant name still serves every set on the page.

<div class="tabset">
  <label><input type="radio" name="tab" checked> Overview</label>
  <div>
    <h3>Overview</h3>
    <form class="tabset vertical">
      <label><input type="radio" name="tab" checked> Details</label>
      <div>…</div>
      <label><input type="radio" name="tab"> History</label>
      <div>…</div>
    </form>
  </div>
  <label><input type="radio" name="tab"> Rules</label>
  <div> … </div>
</div>
An outer div set, each tab holding a vertical form set; every radio named tab