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.

A tab set

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. Pick another tab and the stylesheet swaps which content shows. The radios carry the state, so it holds without a line of script. Every tab follows the same shape: a label with a radio, then the block of content it reveals.

Overview

The content of the first tab.

Rules

The content of the second tab.

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>The content of the first tab.</p>
  </div>
  <label><input type="radio" name="tabs-basic"> Rules</label>
  <div>
    <h3>Rules</h3>
    <p>The content of the second tab.</p>
  </div>
  <label><input type="radio" name="tabs-basic"> Log</label>
  <div>
    <h3>Log</h3>
    <p>Mark one radio <code>checked</code> and that tab opens on load.</p>
  </div>
</div>
Three 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, with no wrapper around it.</p>
  <label><input type="radio" name="tabs-el"> People</label>
  <div>
    <table>
      <thead><tr><th>Login</th><th>Group</th></tr></thead>
      <tbody>
        <tr><td>ada</td><td>Editors</td></tr>
        <tr><td>grace</td><td>Viewers</td></tr>
      </tbody>
    </table>
  </div>
  <label><input type="radio" name="tabs-el"> Actions</label>
  <div class="buttons">
    <button>Save</button>
    <button class="secondary">Duplicate</button>
    <button class="ghost">Delete</button>
  </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.

This radio's value is overview.

This radio's value is rules.

<div class="tabset">
  <label><input type="radio" name="view" value="overview" checked> Overview</label>
  <p>This radio's value is <code>overview</code>.</p>
  <label><input type="radio" name="view" value="rules"> Rules</label>
  <p>This radio's value is <code>rules</code>.</p>
</div>
A value on each radio, to read from script
const tabs = document.querySelector('.tabset');
tabs.addEventListener('change', e => console.log(e.target.value));
tabs.querySelector('input:checked').value;   // the current tab
The script: the checked radio is the current tab

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

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

Selecting an inner tab leaves the outer tab untouched.

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><p>The inner tabs stack down the left, their content beside them.</p></div>
      <label><input type="radio" name="tab"> History</label>
      <div><p>Selecting an inner tab leaves the outer tab untouched.</p></div>
    </form>
  </div>
  <label><input type="radio" name="tab"> Rules</label>
  <div>
    <h3>Rules</h3>
    <form class="tabset vertical">
      <label><input type="radio" name="tab" checked> Access</label>
      <div><p>A separate form, so its selection is independent of the set above.</p></div>
      <label><input type="radio" name="tab"> Limits</label>
      <div><p>One constant name still serves every set on the page.</p></div>
    </form>
  </div>
</div>
An outer div set, each tab holding a vertical form set; every radio named tab