Skip to content
75NeoUI

component · react

Tabs

A row of triggers and the panel the selected one shows, in two variants across three sizes and seven colors.

tabs

Your name, your handle, and the avatar everyone sees.
A new one, twice.

Tabs take their content as items, the way the Accordion does. One array describes both halves: each entry is a trigger and the panel that trigger shows.

const items = [
  { value: "account", label: "Account", content: "Your name and your handle." },
  { value: "billing", label: "Billing", content: "The card on file." },
];

<Tabs items={items} defaultValue="account" />;
<script setup lang="ts">
const items = [
  { value: "account", label: "Account", content: "Your name and your handle." },
  { value: "billing", label: "Billing", content: "The card on file." },
];
</script>

<template>
  <Tabs :items="items" default-value="account" />
</template>

An item can carry an icon shown before its label and a disabled flag, which disables that one trigger without disabling the set.

Panels with more than a sentence

content on the item is a string, which covers a caption and not much else. Anything richer goes through the adapter’s own escape hatch: renderContent in React and the scoped content slot in Vue, both of which receive the item and fall back to its string. renderLabel and the label slot do the same for a trigger.

<Tabs items={items} renderContent={(item) => <InvoiceTable period={item.value} />} />
<Tabs :items="items">
  <template #content="{ item }">
    <InvoiceTable :period="item.value" />
  </template>
</Tabs>

The two variants spend the color differently

A pill is a raised surface, so the indicator stays neutral and the color goes on the selected label. A link has no surface, so the color is the line under the trigger. Both read the same color prop; what changes is where it lands.

The indicator

One element does the sliding, and it is worth knowing how, because it is the only part of this component that is not obvious from the markup. Ark measures the selected trigger and writes its position and size onto the indicator as custom properties, then sets one axis itself: left when the tabs are horizontal, top when they are vertical. The other axis belongs to the recipe. That is what lets the same element be a pill behind the trigger in one variant and a two-pixel line along the list’s edge in the other.

Orientation is not a variant

orientation is horizontal or vertical, and every slot styles itself off the attribute Ark writes rather than off a variant. That keeps the matrix at forty two combinations instead of eighty four, and it is what the Accordion does for the same reason.

Selection

React takes value with onValueChange, or defaultValue to leave the selection alone. Vue takes v-model, with defaultValue as the uncontrolled counterpart.

activationMode decides whether arrowing onto a trigger selects it. It is automatic by default, which is right when the panels are already loaded; manual is for panels that fetch, so a reader arrowing past three tabs does not start three requests.

Slots

The same word names the recipe slot, the data-slot attribute and the key in ui.

  • base
  • list
  • indicator
  • trigger
  • leadingIcon
  • label
  • content

Variants

Read off the recipe, so these are the values that actually resolve. A Theme layer can change which one is the default; it cannot add a value.

variant
pilllink
color
primarysecondarysuccessinfowarningerrorneutral
size
smmdlg

Props

Taken by both adapters, read out of the component module. The same names and the same types work in React and Vue.

ui

TabsUI

Per-slot class overrides.

variant

"pill" | "link"

Defaults to "pill"

color

"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"

Defaults to "primary"

size

"sm" | "md" | "lg"

Defaults to "md"

itemsrequired

TabsItem<ReactNode>[]

The tabs to render, in order.

activationMode

"automatic" | "manual"

Whether arrowing onto a trigger selects it.

Defaults to "automatic"

orientation

"horizontal" | "vertical"

Defaults to "horizontal"

unmountOnExit

boolean

Take the panels out of the DOM while their tab is not selected.

lazyMount

boolean

Wait until first selection to mount a panel.

React only

Also takes Omit<React.HTMLAttributes<HTMLDivElement>, "color" | "defaultValue" | "dir">, and Pick<TabsRootProps, "value" | "defaultValue" | "onValueChange" | "ids">. A className among them reaches the base slot.

renderLabel

(item: TabsItem<React.ReactNode>) => React.ReactNode

Replace a tab's trigger text with arbitrary markup. Falls back to `item.label`.

renderContent

(item: TabsItem<React.ReactNode>) => React.ReactNode

Replace a tab's panel with arbitrary markup. Falls back to `item.content`.