<u-tabs>
<u-tabs> is not a native HTML element, but follows ARIA authoring practices for tabs. It lets you navigate between groups of information that appear in the same context.
Quick intro:
- Use
<u-tabs>to group all tabbing-elements - Use
<u-tablist>around multiple<u-tab>direct children - these are the labels - Use
<u-tab aria-selected="true">to set the open tab (defaults to first non-disabled tab, unless any tab hasaria-selected) - Use
<u-tab aria-controls="id-of-panel">if you need panels outside<u-tabs> - Use
<u-tabpanel>s after<u-tablist>- these hide/show content of related<u-tab> - ARIA Authoring Practices Guide Docs: Tabs
Example
<u-tabs>
<u-tablist>
<u-tab>Tab 1</u-tab>
<u-tab>Tab 2</u-tab>
<u-tab>Tab 3</u-tab>
</u-tablist>
<u-tabpanel>Panel 1 with <a href="#">link</a></u-tabpanel>
<u-tabpanel>Panel 2 with <a href="#">link</a></u-tabpanel>
<u-tabpanel>Panel 3 with <a href="#">link</a></u-tabpanel>
</u-tabs>
<style>
/* Styling just for example: */
u-tab { padding: .5em }
u-tab[aria-selected="true"] { border-bottom: 4px solid }
u-tabpanel { padding: 1em; border: 1px solid }
</style>
Install
npm add -S @u-elements/u-tabspnpm add -S @u-elements/u-tabsyarn add @u-elements/u-tabsbun add -S @u-elements/u-tabs<script type="module" src="https://unpkg.com/@u-elements/u-tabs@latest/dist/u-tabs.js"></script>Attributes and props
<u-tabs>
- Attributes: all global HTML attributes such as
id,class,data- - DOM interface:
UHTMLTabsElementextendsHTMLElementUHTMLTabsElement.tabListreturns the containedUHTMLTabListElementUHTMLTabsElement.selectedIndexsets or gets anumberreflecting the index of the first selected<u-tab>element. Will ignore invalid indexes. Setting it before the tabs exist is ignored like on a native<select>, so when rendering from state, usearia-selected="true"on the<u-tab>instead.UHTMLTabsElement.tabsreturns aNodeListOf<UHTMLTabElement>UHTMLTabsElement.panelsreturns aNodeListOf<UHTMLTabPanelElement>
<u-tablist>
- Attributes: all global HTML attributes such as
id,class,data- - DOM interface:
UHTMLTabListElementextendsHTMLElementUHTMLTabListElement.tabsElementreturns the parentUHTMLTabsElementUHTMLTabListElement.selectedIndexsets or gets anumberreflecting the index of the first selected<u-tab>element. Will ignore invalid indexes. Setting it before the tabs exist is ignored like on a native<select>, so when rendering from state, usearia-selected="true"on the<u-tab>instead.UHTMLTabListElement.tabsreturns aNodeListOf<UHTMLTabElement>
<u-tab>
- Attributes: all global HTML attributes such as
id,class,data-aria-selectedcan contain"true"or"false"to set the currently selected tab. If no tab hasaria-selected, the first non-disabled tab is selected automatically.aria-controlscan contain ID the<u-tabpanel>to control
- DOM interface:
UHTMLTabElementextendsHTMLElementUHTMLTabElement.tabsElementreturns the parentUHTMLTabsElementUHTMLTabElement.selectedsets or getstrueorfalse, indicating whether this tab is currently selectedUHTMLTabElement.indexreturns anumberrepresenting the position/index within the list of tabsUHTMLTabElement.panelreturns the associatedUHTMLTabPanelElement
<u-tabpanel>
- Attributes: all global HTML attributes such as
id,class,data- - DOM interface:
UHTMLTabPanelElementextendsHTMLElementUHTMLTabPanelElement.tabsElementreturns the parentUHTMLTabsElementUHTMLTabPanelElement.tabsreturns a associatedNodeListOf<UHTMLTabElement>
Events
No custom events beyond the usual events supported by HTML elements. Tabbing triggers a click (both with mouse and keyboard) so listen for this to check for tab change:
document.addEventListener('click', ({ target }) => {
const tab = target instanceof Element && target.closest('u-tab');
if (tab) {
console.log('changed to tab:', tab);
}
})Important:
If you're using a framework that delegates click events, call event.preventDefault() during the capture phase to prevent the tab from changing. For example, use onClickCapture in React and Solid, capture:click in Qwik, and onclickcapture in Svelte.
Styling
<u-tabs>, <u-tablist> and <u-tabpanel> renders as display: block, while <u-tab> renders as display: inline-block.
Styling the tab state
<u-tab> automatically gets a aria-selected="true" or aria-selected="false" attribute, which can be utilized for styling:
.my-tab[aria-selected="true"] {
/* Your active tab styling here */
}
.my-tab[aria-selected="false"] {
/* Your inactive tab styling here */
}Styling example: Scrolling tablist
<style>
.my-tablist-scrolls {
display: flex;
overflow: auto;
white-space: nowrap;
}
</style>
<u-tabs>
<u-tablist class="my-tablist-scrolls">
<u-tab>Tab 1</u-tab><u-tab>Tab 2</u-tab><u-tab>Tab 3</u-tab><u-tab>Tab 4</u-tab><u-tab>Tab 5</u-tab><u-tab>Tab 6</u-tab><u-tab>Tab 7</u-tab>
</u-tablist>
<u-tabpanel>Panel 1</u-tabpanel>
</u-tabs>
Styling example: Wrapping tablist
<style>
.my-tablist-wrapps {
display: flex;
flex-wrap: wrap;
}
</style>
<u-tabs>
<u-tablist class="my-tablist-wrapps">
<u-tab>Tab 1</u-tab><u-tab>Tab 2</u-tab><u-tab>Tab 3</u-tab><u-tab>Tab 4</u-tab><u-tab>Tab 5</u-tab><u-tab>Tab 6</u-tab><u-tab>Tab 7</u-tab>
</u-tablist>
<u-tabpanel>Panel 1</u-tabpanel>
</u-tabs>
Server side rendering
You can server side render <u-tabs> by using Declarative Shadow DOM. Example:
import { UHTMLTabsShadowRoot, UHTMLTabListShadowRoot, UHTMLTabShadowRoot, UHTMLTabPanelShadowRoot } from '@u-elements/u-tabs';
const renderToStaticMarkup = (tabs: string[], panels[]) =>
`<u-tabs>
${UHTMLTabsShadowRoot}
<u-tablist>
${UHTMLTabListShadowRoot}
${tabs.map((tab: string) =>
`<u-tab>${UHTMLTabShadowRoot}${tab}</u-tab>`
).join('')}
</u-tablist>
${panels.map((panel: string) =>
`<u-tabpanel>${UHTMLTabPanelShadowRoot}${panel}</u-tabpanel>`
).join('')}
</u-tabs>`Hydration: Attributes are dynamically added in the browser, so the client DOM will differ from the server markup. Use suppressHydrationWarning in React, data-allow-mismatch="attribute" in Vue, or similar for your framework on these elements, to avoid hydration warnings.
Accessibility
| Screen reader | <u-tabs> |
|---|---|
| VoiceOver (Mac) + Chrome | ✅ |
| VoiceOver (Mac) + Edge | ✅ |
| VoiceOver (Mac) + Firefox | ✅ |
| VoiceOver (Mac) + Safari | ✅ |
| VoiceOver (iOS) + Safari | ✅ |
| Jaws (PC) + Chrome | ✅ |
| Jaws (PC) + Edge | ✅ |
| Jaws (PC) + Firefox | ✅ |
| NVDA (PC) + Chrome | ✅ |
| NVDA (PC) + Edge | ✅ |
| NVDA (PC) + Firefox | ✅ |
| Narrator (PC) + Chrome | ✅ |
| Narrator (PC) + Edge | ✅ |
| Narrator (PC) + Firefox | ✅ |
| TalkBack (Android) + Chrome | ✅ |
| TalkBack (Android) + Firefox | ✅ |
| TalkBack (Android) + Samsung Internet | ✅ |
Specifications
- DOM interface: HTMLElement
- ARIA Authoring Practices: Tabs
Changelog
- 2.0.0:
- The first enabled tab is only selected by default when no tab has
aria-selected. If any tab hasaria-selectedand none is"true"(i.e.aria-selected="false"on a single tab), no tab is selected and no panel is shown aria-disabled="true"only blocks user selection (click and keyboard).aria-selected,selectedandselectedIndexcan select a disabled tab, like a disabled<option>, so only one tab is ever selectedselectedIndexcan be read and written before the element is connected, as frameworks set properties on a detached tree (i.e. React and Vue)<u-tabpanel>re-associates with its<u-tab>whenidis written after connect (i.e. Angular binds attributes after inserting the element)- Tabs, tablists and panels are also matched by instance, so custom tag names extending the classes work before connect
- Framework typings for Qwik 1, Qwik 2 and Preact 11 uses
HTMLElementTagNameMap
- The first enabled tab is only selected by default when no tab has
- 1.0.4: Allow no tab to be selected for better React async render compatibility
- 1.0.3: Fix issue where aria-controls combined with aria-selected was not repspected on initial mount
- 1.0.2: Performance optimizations
- 1.0.1: Instant attribute changes for easier testing
- 1.0.0: Plays nice with strict CSP, and renders attributes in consistent order
- 0.1.2: Enhanced React compatibility by removing
u-tabsunknown attribute warning - 0.1.1: Enhanced performance by not relying on UTabsElement MutationObserver
- 0.1.0: Allows changing tag name when extending CustomElement class
- 0.0.12: Respect
aria-disabled="true" - 0.0.11: Enable declarative shadow root support and export
UHTMLTabsShadowRoot,UHTMLTabListShadowRoot,UHTMLTabShadowRootandUHTMLTabPanelShadowRootfor easier server side rendering - 0.0.10: Automatically adds
aria-hidden="true"whenhiddento prevent Safari 18.6 from announcing hidden panels