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%.
<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.
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.
<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:
- Put
role="tablist"and a name on a<div class="tabs">. - Put
role="tab"witharia-controlson each link. - Put
role="tabpanel"witharia-labelledbyon 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.
<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.
<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.
<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.
<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>