Skip to content

ExpressiveCSS

Browse documentation

Tabs

Material Design 3 tabs, from the HTML.

View as Markdown

Build the bar from a <nav class="tabs"> of <a href="#panel"> links. A <span> (or the link text) is the label, and a leading <span class="material-symbols" aria-hidden="true"> is the icon. For a count, nest a <span class="badge"> in the icon and put the count in the tab's aria-label, so screen readers still announce it when the icon is hidden. Add .active to the selected tab. AutoInit() starts every .tabs except those marked no-autoinit.

Each tab is a link and a Tab stop. When a tab has focus, Left and Right move focus to the next or previous tab and wrap at the ends. Home and End move focus to the first and last tab. Focus skips disabled tabs. In a right-to-left page, the arrows still move the way they point. Moving focus does not switch the panel. Enter follows the link. The tabs keep their link semantics and aria-current. For panels that switch in place, a tablist works the other way. When the bar scrolls, the selected tab scrolls into view.

Tokens follow the M3 tabs spec. Primary tabs sit on surface and are 48dp tall, or 64dp with a stacked icon. The label is title-small in on-surface-variant, and primary when selected. The indicator is 3dp primary with 3dp top corners. The icon is 24dp. A 1dp outline-variant divider runs under the bar. Hover is 8%, focus is 10%, and disabled is 38%.

Flight
Luggage
Explore
<nav class="tabs" aria-label="Travel">
  <a href="#flight">
    <span class="material-symbols" aria-hidden="true">flight</span>
    <span>Flight</span>
  </a>
  <a class="active" aria-current="page" href="#luggage" aria-label="Luggage, 3 unread">
    <span class="material-symbols" aria-hidden="true">luggage<span class="badge">3</span></span>
    <span>Luggage</span>
  </a>
  <a href="#explore">
    <span class="material-symbols" aria-hidden="true">explore</span>
    <span>Explore</span>
  </a>
</nav>
<div id="flight">Flight</div>
<div id="luggage">Luggage</div>
<div id="explore">Explore</div>

Add disabled on the <a> to make it inaccessible. Tabs become scrollable when there are too many to fit.

Test 1
Test 2
Test 3
Test 4
Test 5
Test 6
Test 7
Test 8
Test 9
Test 10
Test 11

Secondary

Add tabs-secondary for the in-content variant. Use it to split a pane further. It is always 48dp, with inline icons and a 2dp indicator.

Travel
Hotel
Walks
<nav class="tabs tabs-secondary" aria-label="Trip">
  <a href="#travel">
    <span class="material-symbols" aria-hidden="true">flight</span>
    <span>Travel</span>
  </a>
  <a class="active" aria-current="page" href="#hotel">
    <span class="material-symbols" aria-hidden="true">hotel</span>
    <span>Hotel</span>
  </a>
</nav>

Tablist

For panels that switch in place, you can make the strip a tablist instead. Link tabs stay the default, for tabs that lead to sections or pages. To build a tablist:

  1. Put role="tablist" and a name on a <div class="tabs">.
  2. Put role="tab" with aria-controls on each link.
  3. Put role="tabpanel" with aria-labelledby on each panel.

Tabs then keeps aria-selected on the selected tab and a roving tabindex, so the strip is one Tab stop. Left and Right, Home and End move between the tabs and select as they go.

Flight
Hotel
Car
<div class="tabs" role="tablist" aria-label="Trip details">
  <a role="tab" id="flight-tab" href="#flight" aria-controls="flight">Flight</a>
  <a role="tab" class="active" id="hotel-tab" href="#hotel" aria-controls="hotel">Hotel</a>
  <a role="tab" id="car-tab" href="#car" aria-controls="car">Car</a>
</div>
<div id="flight" role="tabpanel" aria-labelledby="flight-tab">Flight</div>
<div id="hotel" role="tabpanel" aria-labelledby="hotel-tab">Hotel</div>
<div id="car" role="tabpanel" aria-labelledby="car-tab">Car</div>

Initialization

The IIFE bundle exposes Expressive.Tabs. Call init yourself when you need options other than the defaults, or let Expressive.AutoInit() start every .tabs.

document.addEventListener('DOMContentLoaded', function() {
  const elems = document.querySelectorAll('.tabs');
  const instances = Expressive.Tabs.init(elems, {
    swipeable: false
  });
});

Options

Name Type Default Description
duration Number null Indicator transition duration in milliseconds, on an ease-out curve. The default, null, moves the indicator on the default spatial spring (--md-comp-tabs-indicator-easing and --md-comp-tabs-indicator-duration), which follows motion="expressive" and stops under reduced motion.
onShow Function null Called when a new tab panel is shown. Receives the panel element.
swipeable Boolean false Enable swipeable tabs. Uses responsiveThreshold. Moves the panels into a scroll-snap track.
responsiveThreshold Number Infinity Maximum viewport width, in pixels, at which swipeable mode starts. Wider viewports stay non-swipeable.

Methods

All methods are called on the plugin instance. You can get the instance like this:
const instance = Expressive.Tabs.getInstance(elem);
.select();

Show the panel that belongs to the tab with this id. Tabs matches panel IDs literally in normal and swipeable modes, including numeric IDs and IDs containing dots, colons or brackets.

String: The id of the tab panel (without #).

instance.select('tab_id');
.updateTabIndicator();

Recalculate the indicator position. Call it after the bar has been hidden or resized.

instance.updateTabIndicator();
.destroy();

Destroy the plugin instance and tear down its event handlers.

instance.destroy();

Properties

Name Type Description
el Element The DOM element the plugin was initialized with (the .tabs bar).
options Object The options the instance was initialized with.
index Number Index of the tab that is currently shown.

Preselecting a tab

The first tab is selected by default. To pick another, add active to its <a>, or load the page with a matching hash such as #test2.

<a class="active" href="#test3">Test 3</a>

Linking to an external page

To keep a tab as a normal hyperlink, set target on the link. Without it, Tabs ignores default anchor behaviour.

<nav class="tabs" aria-label="Sections">
  <a target="_blank" href="https://github.com">External link in new window</a>
  <a target="_self" href="https://github.com">External link in same window</a>
</nav>

Swipeable tabs

Set swipeable: true to swipe between panels with touch or a trackpad. Keep the tab panels in the same wrapping container. Tabs moves those panels into a full-width .tabs-content scroll-snap track. Only the shown panel is reachable by keyboard.

responsiveThreshold is the maximum viewport width at which swipeable mode initializes. Resizing later does not switch modes. Clicks and swipes update the current link. They call onShow once per changed selection, with the shown panel. destroy() removes the track and restores panel positions and the state owned by Tabs.

This demo is marked no-autoinit and started with swipeable: true.

Test 1
Test 2
Test 3
<nav id="tabs-swipe-demo" class="tabs" aria-label="Sections">
  <a href="#test-swipe-1">Test 1</a>
  <a class="active" aria-current="page" href="#test-swipe-2">Test 2</a>
  <a href="#test-swipe-3">Test 3</a>
</nav>
Expressive.Tabs.init(document.querySelector('#tabs-swipe-demo'), {
  swipeable: true
});

Fixed width

Add max so every tab grows equally. On compact viewports every tab bar already uses this layout.

Test 1
Test 2
Test 3
Test 4
Test 5
<nav class="tabs max" aria-label="Sections">
  <a href="#test1">Test 1</a>
  <a class="active" aria-current="page" href="#test2">Test 2</a>
  <a class="disabled" href="#test3">Disabled</a>
</nav>

Inline icons

Primary tabs stack the icon above the label (64dp). Add horizontal to put them on one line, like secondary tabs.

Flight
Luggage
Explore
<nav class="tabs max horizontal" aria-label="Sections">
  <a href="#flight">
    <span class="material-symbols" aria-hidden="true">flight</span>
    <span>Flight</span>
  </a>
  <a class="active" aria-current="page" href="#luggage">
    <span class="material-symbols" aria-hidden="true">luggage</span>
    <span>Luggage</span>
  </a>
</nav>

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow