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/react

Usage

import { Tab, TabLink, TabList, TabNav, TabPanel, Tabs } from "@harmo-ui/react";

<TabNav aria-label="Analytics">
  <TabLink href="/analytics/tickets" current>
    Tickets
  </TabLink>
  <TabLink href="/analytics/campaigns">Campaigns</TabLink>
</TabNav>

<Tabs defaultValue="all">
  <TabList aria-label="Quick replies">
    <Tab value="all">All</Tab>
    <Tab value="private">Private</Tab>
  </TabList>
  <TabPanel value="all">Every quick reply you can use.</TabPanel>
  <TabPanel value="private">Replies only you can see.</TabPanel>
</Tabs>

Examples

This site is using your theme

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.

const [current, setCurrent] = useState("/analytics/tickets");
const links = [
  ["/analytics/tickets", "Tickets"],
  ["/analytics/campaigns", "Campaigns"],
  ["/analytics/ai-agents", "AI agents"],
  ["/analytics/satisfaction", "Customer satisfaction"],
];

return (
  <TabNav aria-label="Analytics">
    {links.map(([href, text]) => (
      <TabLink
        key={href}
        href={href}
        current={href === current}
        onClick={() => setCurrent(href)}
      >
        {text}
      </TabLink>
    ))}
  </TabNav>
);

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.

Every quick reply you can use.
<Tabs defaultValue="all">
  <TabList aria-label="Quick replies">
    <Tab value="all">All</Tab>
    <Tab value="private">Private</Tab>
    <Tab value="team">Team</Tab>
    <Tab value="account">Account</Tab>
  </TabList>
  <TabPanel value="all">Every quick reply you can use.</TabPanel>
  <TabPanel value="private">Replies only you can see.</TabPanel>
  <TabPanel value="team">Replies shared with your team.</TabPanel>
  <TabPanel value="account">
    Replies everyone in the account can use.
  </TabPanel>
</Tabs>

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.

const [current, setCurrent] = useState("/settings/billing");
const links = [
  ["/settings/account", "Account details"],
  ["/settings/hours", "Business hours"],
  ["/settings/messaging", "Messaging"],
  ["/settings/notifications", "Notifications"],
  ["/settings/team", "Team members"],
  ["/settings/billing", "Billing"],
  ["/settings/integrations", "Integrations"],
  ["/settings/developers", "Developers"],
];

return (
  <TabNav aria-label="Settings">
    {links.map(([href, text]) => (
      <TabLink
        key={href}
        href={href}
        current={href === current}
        onClick={() => setCurrent(href)}
      >
        {text}
      </TabLink>
    ))}
  </TabNav>
);

Right to left

The same tabs in Arabic, starting from the right.

const [current, setCurrent] = useState("/analytics/tickets");
const links = [
  ["/analytics/tickets", "التذاكر"],
  ["/analytics/campaigns", "الحملات"],
  ["/analytics/ai-agents", "وكلاء الذكاء الاصطناعي"],
  ["/analytics/satisfaction", "رضا العملاء"],
];

return (
  <TabNav aria-label="التحليلات">
    {links.map(([href, text]) => (
      <TabLink
        key={href}
        href={href}
        current={href === current}
        onClick={() => setCurrent(href)}
      >
        {text}
      </TabLink>
    ))}
  </TabNav>
);

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.

PropOnEffect
currentTabLinkMarks the current page with aria-current="page"
alwaysShowTabNav, TabListShows the strip even with one tab
value or defaultValueTabsThe selected tab's value; one of them is required
onValueChangeTabsCalled with the newly selected value
valueTab, TabPanelPairs 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.

HookEffect
aria-current="page" on a linkThe current tab
aria-selected="true" on a role="tab" buttonThe selected tab, drawn like the current link
hidden on a <li> or a role="tab" buttonHides that tab, with no gap left
data-ready on .htabsSet by the script: the sliding pill and underline take over
data-on-current on .htabs-highlightSet 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 an aria-label, the current one marked aria-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 an aria-label holds role="tab" buttons, each pointing at its role="tabpanel" with aria-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.