Skip to main content

Tabs

<zeno-tabs>NavigationSource

A row of tabs that shows one panel at a time.

import '@usezeno/tokens/css';
import '@usezeno/core/web-components';

Examples​

Basic​

Tabs are set with the tabs property. Each child of the element is a panel, and panels are matched to tabs by their order. Set value to the tab that is open at the start.

Overview: 3 projects, 12 open tasks.

Activity: the last change was made today.

Settings: choose who can see this page.

<zeno-tabs value="overview">
<div><p>Overview: 3 projects, 12 open tasks.</p></div>
<div><p>Activity: the last change was made today.</p></div>
<div><p>Settings: choose who can see this page.</p></div>
</zeno-tabs>
document.querySelector('zeno-tabs').tabs = [
{ value: 'overview', label: 'Overview' },
{ value: 'activity', label: 'Activity' },
{ value: 'settings', label: 'Settings' },
];

Variants​

Line: tasks for today.

Line: tasks for this week.

Pills: tasks for today.

Pills: tasks for this week.

Card: tasks for today.

Card: tasks for this week.

<zeno-tabs variant="line" value="day">
<div><p>Line: tasks for today.</p></div>
<div><p>Line: tasks for this week.</p></div>
</zeno-tabs>
<zeno-tabs variant="pills" value="day">
<div><p>Pills: tasks for today.</p></div>
<div><p>Pills: tasks for this week.</p></div>
</zeno-tabs>
<zeno-tabs variant="card" value="day">
<div><p>Card: tasks for today.</p></div>
<div><p>Card: tasks for this week.</p></div>
</zeno-tabs>
document.querySelectorAll('zeno-tabs').forEach(tabs => {
tabs.tabs = [
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
];
});

Sizes and full width​

full-width makes the tabs share the whole width of the element.

Small tabs.

Shared files.

Large tabs.

Shared files.

Full width tabs.

Shared files.

<zeno-tabs size="sm" value="files">
<div><p>Small tabs.</p></div>
<div><p>Shared files.</p></div>
</zeno-tabs>
<zeno-tabs size="lg" value="files">
<div><p>Large tabs.</p></div>
<div><p>Shared files.</p></div>
</zeno-tabs>
<zeno-tabs full-width value="files">
<div><p>Full width tabs.</p></div>
<div><p>Shared files.</p></div>
</zeno-tabs>
document.querySelectorAll('zeno-tabs').forEach(tabs => {
tabs.tabs = [
{ value: 'files', label: 'My files' },
{ value: 'shared', label: 'Shared' },
];
});

Vertical​

Profile: your name and photo.

Password: change your password.

Notifications: choose what we send you.

<zeno-tabs vertical value="profile">
<div><p>Profile: your name and photo.</p></div>
<div><p>Password: change your password.</p></div>
<div><p>Notifications: choose what we send you.</p></div>
</zeno-tabs>
document.querySelector('zeno-tabs').tabs = [
{ value: 'profile', label: 'Profile' },
{ value: 'password', label: 'Password' },
{ value: 'notifications', label: 'Notifications' },
];

Icons, badges, a disabled tab and the change event​

A tab can have an icon, a badge and disabled. The change event carries the new value in event.detail.

Home

Favourites

Account

Admin

Selected: home

<zeno-tabs value="home">
<div><p>Home</p></div>
<div><p>Favourites</p></div>
<div><p>Account</p></div>
<div><p>Admin</p></div>
</zeno-tabs>
<p data-result>Selected: home</p>
const tabs = document.querySelector('zeno-tabs');
tabs.tabs = [
{ value: 'home', label: 'Home', icon: 'home' },
{ value: 'favourites', label: 'Favourites', icon: 'star', badge: 4 },
{ value: 'account', label: 'Account', icon: 'user' },
{ value: 'admin', label: 'Admin', icon: 'settings', disabled: true },
];
tabs.addEventListener('change', event => {
document.querySelector('[data-result]').textContent = 'Selected: ' + event.detail.value;
});

Attributes and properties​

PropertyAttributeTypeDefaultDescription
tabstabsTabItem[][]
valuevaluestring''
variantvariantreflects'line' | 'pills' | 'card''line'
sizesizereflects'sm' | 'md' | 'lg''md'
verticalverticalreflectsbooleanfalse
fullWidthfull-widthreflectsbooleanfalse
testIdstringTest ID for automated testing From ZenoElement.

Methods​

MethodDescription
selectTab(value: string): voidSelect a tab
focus(): voidFocus the component or its first focusable element From ZenoElement.
blur(): voidBlur the component or its focused element From ZenoElement.

Events​

EventDetailDescription
change{ value: string; tab: TabItem }A different tab was selected.

Slots​

SlotDescription
DefaultThe panels, one per tab.

CSS parts​

Part
badge
base
icon
label
list
panels
tab

CSS custom properties​

This element documents no custom properties of its own. Style it through its parts and through the tokens it reads.

The element's own source reads no tokens.

Types​

interface TabItem {
value: string;
label: string;
icon?: string;
disabled?: boolean;
badge?: string | number;
}

Accessibility​

Keyboard​

KeyWhat it does
TabMoves focus to the first tab that is not disabled, then to each next tab, then into the page after the tabs.
ArrowRightArrowDownMoves focus to the next tab that is not disabled and selects it. From the last tab it goes to the first.
ArrowLeftArrowUpMoves focus to the previous tab that is not disabled and selects it. From the first tab it goes to the last.
HomeEndMoves focus to the first or last tab that is not disabled and selects it.
EnterSpaceSelects the focused tab.

Forms​

This element is not a form control. It adds nothing to a form's data.

Notes​

  • The row of tabs is a <div role="tablist">. Each tab is a native <button> with role="tab" and aria-selected. A disabled tab has the native disabled attribute.
  • Each child of the element gets role="tabpanel". Every panel except the selected one gets the hidden attribute.
  • Selection follows focus: an arrow key selects the tab it moves to.
  • With prefers-reduced-motion: reduce the tab transitions are turned off.

Tests and known gaps​

  • No tests of its own. The only check is that the element registers and renders (smoke.test.ts).
  • Not checked with axe-core.
  • Has no Storybook story.
13 of the 19 properties, methods, events, slots and parts above have no description in the source
  • the property tabs
  • the property value
  • the property variant
  • the property size
  • the property vertical
  • the property fullWidth
  • the part badge
  • the part base
  • the part icon
  • the part label
  • the part list
  • the part panels
  • the part tab

Known gaps:

  • Without value, no tab is selected and every panel is hidden. The code picks the first tab, but the next step sets the active tab back to the empty value. Always set value.
  • A value that matches no tab also leaves every panel hidden and no tab selected.
  • Every enabled tab is a tab stop. The tab list does not use a roving tabindex, so the Tab key goes through all tabs before it reaches the panel.
  • Each tab has aria-controls="panel-<value>", but no panel is given that id.
  • Each panel has aria-labelledby="tab-<value>". The tab with that id is inside the shadow root, so the reference does not resolve and the panel has no name.
  • A vertical tab list has no aria-orientation="vertical".
  • The tab list has no accessible name, and there is no attribute to give it one.
  • In the line variant the line under the selected tab is not painted. The tab list clips it, so the selected tab is shown by text colour only.
  • The fade-in style for panels is never used, because slotted panels do not get the tabs__panel class.