Tabs
Switches between sibling pages, such as the sections of Analytics or Settings, or between panels on one page. A hover pill and an underline slide between tabs. For one choice among a few options in a form, use a segmented control.
Installation
bun add @harmo-ui/css @harmo-ui/reactnpm install @harmo-ui/css @harmo-ui/reactpnpm add @harmo-ui/css @harmo-ui/reactyarn add @harmo-ui/css @harmo-ui/reactbunx harmo-ui add tabsnpx harmo-ui add tabspnpm dlx harmo-ui add tabsyarn dlx harmo-ui add tabsUsage
Examples
Tabs
Links between sibling pages: a <nav> with aria-current="page" on the current link. A script slides the hover pill after the pointer and the underline to a new current tab. Here clicking moves the current tab instead of leaving the page.
<nav class="htabs" aria-label="Analytics">
<ul>
<li><a href="/analytics/tickets" aria-current="page">Tickets</a></li>
<li><a href="/analytics/campaigns">Campaigns</a></li>
<li><a href="/analytics/ai-agents">AI agents</a></li>
<li><a href="/analytics/satisfaction">Customer satisfaction</a></li>
</ul>
<span class="htabs-highlight" aria-hidden="true"></span>
<span class="htabs-indicator" aria-hidden="true"></span>
</nav>Switching panels
The same look for content switched on one page: a role="tablist" of role="tab" buttons, each controlling a role="tabpanel". The selected tab has aria-selected="true" and the others tabindex="-1", so Tab moves on into the panel while the Arrow keys move between tabs. A script selects tabs and shows their panels.
Many tabs
Tabs that don't fit stay on one line, and the strip scrolls on its own while the page stays still. A script keeps the current tab in view, in the middle of the strip where it can: at once on load or after a key press, and with a slide after a click.
Right to left
The same tabs in Arabic, starting from the right.
<nav class="htabs" aria-label="التحليلات">
<ul>
<li><a href="/analytics/tickets" aria-current="page">التذاكر</a></li>
<li><a href="/analytics/campaigns">الحملات</a></li>
<li><a href="/analytics/ai-agents">وكلاء الذكاء الاصطناعي</a></li>
<li><a href="/analytics/satisfaction">رضا العملاء</a></li>
</ul>
<span class="htabs-highlight" aria-hidden="true"></span>
<span class="htabs-indicator" aria-hidden="true"></span>
</nav>API reference
React
For links, <TabNav> renders the <nav>, with a <TabLink> for each link. For panels, <Tabs> holds the selection, <TabList> renders the strip, and each <Tab> and its <TabPanel> share a value, from which their ids and connections are built. Both strips run the highlight and underline, and are hidden when one tab or none is shown, so a lone panel keeps its name. <TabList> handles the Arrow keys, Home, and End. Other props are each element's attributes, except that a <TabList>'s className and style go to the .htabs wrapper, and its other props and ref to the role="tablist" element. Name a <TabNav> or <TabList> with aria-label.
| Prop | On | Effect |
|---|---|---|
current | TabLink | Marks the current page with aria-current="page" |
alwaysShow | TabNav, TabList | Shows the strip even with one tab |
value or defaultValue | Tabs | The selected tab's value; one of them is required |
onValueChange | Tabs | Called with the newly selected value |
value | Tab, TabPanel | Pairs a tab with its panel |
HTML
Between pages, a <nav class="htabs"> around a <ul> of links. On one page, a <div class="htabs"> around a role="tablist" of buttons, each with role="tab", aria-selected, aria-controls, and tabindex="-1" when unselected. Either way, aria-label names it, and .htabs-highlight and .htabs-indicator follow the list. The panels are the application's own; Tabs doesn't style them. Load tabs.css.
| Hook | Effect |
|---|---|
aria-current="page" on a link | The current tab |
aria-selected="true" on a role="tab" button | The selected tab, drawn like the current link |
hidden on a <li> or a role="tab" button | Hides that tab, with no gap left |
data-ready on .htabs | Set by the script: the sliding pill and underline take over |
data-on-current on .htabs-highlight | Set by the script: the pill is over the current tab |
A script is optional for links, for the sliding pill and underline and keeping the current tab in view, and required for panels. Before it runs, or without it, the current tab shows a fixed underline and each tab its own hover background. See Tabs' full contract.
Accessibility
- Between pages: links in a
<nav>with anaria-label, the current one markedaria-current="page". Not an ARIA tablist, which is for panels on one page. - On one page: the WAI-ARIA tabs pattern. A
role="tablist"with anaria-labelholdsrole="tab"buttons, each pointing at itsrole="tabpanel"witharia-controls. Only the selected tab is in the Tab order (tabindex="-1"on the others), so Tab moves from the tabs into the panel, and the Arrow keys move between tabs. - The pill and underline are
aria-hidden.