Tabs New

A set of label elements with hidden radio inputs inside a .tabs container. No JavaScript. CSS :has() handles panel switching.

Standalone

<link rel="stylesheet" href="https://esm.sh/@erikt/ui/tabs.css" />

Default

  • Each label wraps a hidden radio input. Add checked to set the default tab.
    CSS uses :has() to watch the checked state inside [role="tablist"] and show the panel at the matching position, with no JavaScript.
  • Give the header role="tablist" and an aria-label.
    The tab bar styling targets [role="tablist"] directly. Screen readers also use this to announce the group and let users navigate between tabs.
  • Connect each input to its panel with matching id and aria-controls.
    Lets assistive technology jump directly from a tab to its associated content.
  • Mark each panel with role="tabpanel", aria-labelledby, and tabindex="0".
    All [role="tabpanel"] elements are hidden by default. The Nth panel is revealed when the Nth tab is checked. aria-labelledby names the panel after its tab, and tabindex="0" makes it reachable by keyboard.

Manage your account settings and preferences.

Change your password and security settings.

Configure how and when you receive notifications.

<section class="tabs">
  <header role="tablist" aria-label="Account settings">
    <label>
      <input
        type="radio"
        name="tabs"
        id="tab-1"
        checked
        aria-controls="panel-1"
      />
      Account
    </label>
    <label>
      <input type="radio" name="tabs" id="tab-2" aria-controls="panel-2" />
      Password
    </label>
    <label>
      <input type="radio" name="tabs" id="tab-3" aria-controls="panel-3" />
      Notifications
    </label>
  </header>

  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
    <p>Manage your account settings and preferences.</p>
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0">
    <p>Change your password and security settings.</p>
  </div>
  <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0">
    <p>Configure how and when you receive notifications.</p>
  </div>
</section>

Disabled tab

Add disabled to the radio input to disable a tab.

Overview content goes here.

Analytics content goes here.

Reports content goes here.

<section class="tabs">
  <header role="tablist" aria-label="Dashboard">
    <label>
      <input
        type="radio"
        name="tabs"
        id="tab-1"
        checked
        aria-controls="panel-1"
      />
      Overview
    </label>
    <label>
      <input type="radio" name="tabs" id="tab-2" aria-controls="panel-2" />
      Analytics
    </label>
    <label>
      <input
        type="radio"
        name="tabs"
        id="tab-3"
        disabled
        aria-controls="panel-3"
      />
      Reports
    </label>
  </header>

  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
    Overview content goes here.
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0">
    Analytics content goes here.
  </div>
  <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0">
    Reports content goes here.
  </div>
</section>

With content

Tab panels can contain any content.

Name Role Status
Alice Engineer Active
Bob Designer Away
Carol Manager Active

No activity yet

Actions taken by your team will appear here.

Role
  • <section class="tabs" style="display: grid; gap: 1rem">
      <header role="tablist" aria-label="Team">
        <label>
          <input
            type="radio"
            name="tabs"
            id="tab-1"
            checked
            aria-controls="panel-1"
          />
          Members
        </label>
        <label>
          <input type="radio" name="tabs" id="tab-2" aria-controls="panel-2" />
          Activity
        </label>
        <label>
          <input type="radio" name="tabs" id="tab-3" aria-controls="panel-3" />
          Invite
        </label>
      </header>
    
      <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
        <table>
          <thead>
            <tr>
              <th>Name</th>
              <th>Role</th>
              <th>Status</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Alice</td>
              <td>Engineer</td>
              <td>Active</td>
            </tr>
            <tr>
              <td>Bob</td>
              <td>Designer</td>
              <td>Away</td>
            </tr>
            <tr>
              <td>Carol</td>
              <td>Manager</td>
              <td>Active</td>
            </tr>
          </tbody>
        </table>
      </div>
    
      <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0">
        <section class="empty">
          <svg><!-- icon --></svg>
          <h3>No activity yet</h3>
          <p>Actions taken by your team will appear here.</p>
        </section>
      </div>
    
      <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0">
        <form style="display: grid; gap: 1em">
          <label class="field">
            <span>Email address</span>
            <input type="email" placeholder="colleague@example.com" />
          </label>
          <div class="field">
            <span>Role</span>
            <button type="button" class="outlined" popovertarget="role">
              Member
            </button>
            <div id="role" popover>
              <menu>
                <li>
                  <label>
                    <input type="radio" name="role" value="member" checked /> Member
                  </label>
                </li>
                <li>
                  <label>
                    <input type="radio" name="role" value="admin" /> Admin
                  </label>
                </li>
                <li>
                  <label>
                    <input type="radio" name="role" value="viewer" /> Viewer
                  </label>
                </li>
              </menu>
            </div>
          </div>
          <button type="submit">Send invite</button>
        </form>
      </div>
    </section>

    Nested

    Tabs nest as deep as you like. Each level is independent, so switching the outer tabs leaves the inner ones where they were.

    Outer panel one, holding another tabs component:

    There's another tabs component in the other tab

    Woof

    Meow

    Outer panel two.

    <!-- Level 1 -->
    <section class="tabs first">
      <header role="tablist" aria-label="Outer tabs">
        <label> <input type="radio" name="outer" checked /> Overview </label>
        <label><input type="radio" name="outer" /> Settings</label>
      </header>
    
      <div role="tabpanel">
        <p>Outer panel one, holding another tabs component:</p>
        <!-- Level 2 -->
        <section class="tabs second">
          <header role="tablist" aria-label="Inner tabs">
            <label><input type="radio" name="inner" /> Info</label>
            <label> <input type="radio" name="inner" checked /> Animals </label>
          </header>
    
          <div role="tabpanel">
            <p>There's another tabs component in the other tab</p>
          </div>
          <div role="tabpanel">
            <!-- Level 3 -->
            <section class="tabs third">
              <header role="tablist" aria-label="Inner tabs">
                <label> <input type="radio" name="third" checked /> Dog </label>
                <label><input type="radio" name="third" /> Cat</label>
              </header>
    
              <div role="tabpanel"><p>Woof</p></div>
              <div role="tabpanel"><p>Meow</p></div>
            </section>
          </div>
        </section>
      </div>
    
      <div role="tabpanel">
        <p>Outer panel two.</p>
      </div>
    </section>
    
    <style>
      .tabs {
        padding: 1rem;
    
        .first {
          border: 1px solid var(--ui-neutral-100);
        }
    
        .second {
          background-color: var(--ui-neutral-100);
        }
    
        .third {
          background-color: var(--ui-neutral-0);
        }
      }
    </style>

    Custom properties

    Set these on the .tabs element to override its built-in styling. Each one falls back to a global token, so an unset property keeps the tabs in line with the rest of the system.

    Property Default Description
    --bg var(--ui-neutral-200) Background of the tab list.
    --bg-active var(--ui-neutral-50) Background of the highlight behind the selected tab.
    --fg var(--ui-neutral-800) Text color of the tabs.
    --fg-hover var(--ui-neutral-950) Text color of a tab on hover.
    --fg-active var(--ui-neutral-950) Text color of the selected tab.
    .tabs {
      --bg: var(--ui-neutral-100);
      --bg-active: var(--ui-neutral-0);
    }

    API reference

    Type Classes / attributes What it does
    State [checked] On a tab's radio, selects the tab and shows its panel
    Disabled [disabled] On a tab's radio, fades the tab and blocks clicks
    Track --bg: var(--ui-neutral-200) Background of the tab list
    Highlight --bg-active: var(--ui-neutral-50) Background of the selected tab
    Text --fg: var(--ui-neutral-800) Text color of tabs
    Hover text --fg-hover: var(--ui-neutral-950) Text color of a hovered tab
    Active text --fg-active: var(--ui-neutral-950) Text color of the selected tab

    Search 5021 icons

    Type a name to find icons from the Tabler icon set.

        Move Open Esc Close