Aksara UI

Tabs

Tabs switch between related views in the same place.

import { Tabs } from '@aksornproject/aksara-ui'

Basic tabs

Tabs.Root holds the selection. Tabs.List is the tab row and needs an aria-label or aria-labelledby. Each Tabs.Tab and Tabs.Panel pair shares a value, a string or a number. defaultValue picks the first selected tab.

<Tabs.Root defaultValue="overview" className="w-full">
  <Tabs.List aria-label="Course views">
    <Tabs.Tab value="overview">Overview</Tabs.Tab>
    <Tabs.Tab value="lessons">Lessons</Tabs.Tab>
    <Tabs.Tab value="grades">Grades</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="overview">
    Algebra for upper secondary students, taught over 12 weeks.
  </Tabs.Panel>
  <Tabs.Panel value="lessons">
    24 lessons, from linear equations to quadratic functions.
  </Tabs.Panel>
  <Tabs.Panel value="grades">
    Grades appear here after the first assignment.
  </Tabs.Panel>
</Tabs.Root>

Sizes

size on the Root sets the tab height for every tab. small is 40px and the default, medium is 44px and large is 48px.

<div className="flex w-full flex-col gap-spacing-16">
  {sizes.map((size) => (
    <Tabs.Root key={size} size={size} defaultValue="overview">
      <Tabs.List aria-label={`Course views, ${size}`}>
        <Tabs.Tab value="overview">Overview</Tabs.Tab>
        <Tabs.Tab value="lessons">Lessons</Tabs.Tab>
        <Tabs.Tab value="grades">Grades</Tabs.Tab>
      </Tabs.List>
    </Tabs.Root>
  ))}
</div>

Slots and disabled tabs

leading sits before the label. selectedContent shows only while the tab is selected and always reserves its box, so selection never moves the row. Both are decorative. endContent holds compact noninteractive content, such as a count, and its text is part of the tab name.

disabled keeps a tab focusable but it never activates.

<Tabs.Root defaultValue="saved" className="w-full">
  <Tabs.List aria-label="Library views">
    <Tabs.Tab value="search" leading={<IconInterfaceEssentialSearch />}>
      Search
    </Tabs.Tab>
    <Tabs.Tab
      value="saved"
      selectedContent={<IconInterfaceEssentialDoneCheck />}
    >
      Saved
    </Tabs.Tab>
    <Tabs.Tab value="assigned" endContent={<span>3</span>}>
      Assigned
    </Tabs.Tab>
    <Tabs.Tab value="archived" disabled>
      Archived
    </Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="search">
    Search the library by title or topic.
  </Tabs.Panel>
  <Tabs.Panel value="saved">Worksheets you saved for later.</Tabs.Panel>
  <Tabs.Panel value="assigned">
    Three worksheets are due this week.
  </Tabs.Panel>
</Tabs.Root>

Tabs.Menu is a tab that opens a menu of options. Pass Menu content to menu. It has its own value and panel like a regular tab. Click, Enter and Space select it and open the popup. Arrow keys open the popup without selecting.

Selection and the open state stay independent. open, defaultOpen and onOpenChange control the popup like Menu.Root.

<Tabs.Root defaultValue="details" className="w-full">
  <Tabs.List aria-label="Course views">
    <Tabs.Tab value="details">Details</Tabs.Tab>
    <Tabs.Tab value="units">Units</Tabs.Tab>
    <Tabs.Menu
      value="versions"
      menu={
        <>
          <Menu.Item>Published</Menu.Item>
          <Menu.Item>Drafts</Menu.Item>
        </>
      }
    >
      Versions
    </Tabs.Menu>
  </Tabs.List>
  <Tabs.Panel value="details">
    Course title, subject and grade level.
  </Tabs.Panel>
  <Tabs.Panel value="units">Five units, in teaching order.</Tabs.Panel>
  <Tabs.Panel value="versions" keepMounted>
    Pick a version from the menu.
  </Tabs.Panel>
</Tabs.Root>

Fill layout

layout="fill" on the List shares the row equally, with a 96px minimum per tab. Long labels truncate, and the list scrolls when the tabs do not fit. The default hug layout sizes each tab to its label.

<Tabs.Root defaultValue="students" className="w-full max-w-[480px]">
  <Tabs.List aria-label="Class views" layout="fill">
    <Tabs.Tab value="students">Students</Tabs.Tab>
    <Tabs.Tab value="assignments">Assignments</Tabs.Tab>
    <Tabs.Tab value="attendance">Attendance</Tabs.Tab>
  </Tabs.List>
</Tabs.Root>

Actions and overflow

actions holds your buttons after a vertical separator. overflow holds a button or menu trigger at the end of the row. Both sit outside the tablist, so they are not tabs. divider={false} removes the rule under the row.

<Tabs.Root defaultValue="students" className="w-full">
  <Tabs.List
    aria-label="Class views"
    actions={
      <Button size="xs" variant="tertiary-accent">
        Export
      </Button>
    }
    overflow={
      <Menu.Root>
        <Menu.Trigger
          aria-label="More actions"
          render={<button type="button" />}
        >
          <IconInterfaceEssentialDots />
        </Menu.Trigger>
        <Menu.Portal>
          <Menu.Positioner align="end">
            <Menu.Popup>
              <Menu.Item>Print</Menu.Item>
              <Menu.Item>Archive class</Menu.Item>
            </Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.Root>
    }
  >
    <Tabs.Tab value="students">Students</Tabs.Tab>
    <Tabs.Tab value="assignments">Assignments</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="students">28 students enrolled.</Tabs.Panel>
  <Tabs.Panel value="assignments">4 assignments this term.</Tabs.Panel>
</Tabs.Root>

Controlled tabs

Pass value and onValueChange to control the selection. onValueChange reports the next value as string | number | null with the Base UI event details. value and defaultValue cannot be combined, and null selects nothing.

Showing the week view.

<div className="flex w-full flex-col gap-spacing-8">
  <Tabs.Root value={value} onValueChange={setValue}>
    <Tabs.List aria-label="Schedule range">
      <Tabs.Tab value="day">Day</Tabs.Tab>
      <Tabs.Tab value="week">Week</Tabs.Tab>
      <Tabs.Tab value="month">Month</Tabs.Tab>
    </Tabs.List>
  </Tabs.Root>
  <p>Showing the {value} view.</p>
</div>

Accessibility

  • Arrow keys move focus without selecting. Enter and Space select. Home and End jump to the ends, and focus loops by default. activateOnFocus and loopFocus on the List change this.
  • Base UI supplies the roving tabindex and links each tab to its panel.
  • The List requires an accessible name. TypeScript rejects a List without aria-label or aria-labelledby.
  • Menu tabs expose aria-haspopup="menu" and aria-expanded. Escape closes the popup and returns focus to the tab.
  • leading, selectedContent, the chevron, the indicator and the divider are aria-hidden. endContent stays in the accessible name.
  • Only horizontal tabs exist. orientation accepts horizontal alone.

API

Tabs.Root

Prop

Type

Tabs.List

Prop

Type

Tabs.Tab

Prop

Type

Tabs.Menu

Prop

Type

Tabs.Panel

Prop

Type

Edit on GitHub

On this page