nalarx-ui
Components

Tabs

Pill and underline tabs on Base UI, with one thumb or underline that slides to the active tab. Never orange.

bunx shadcn@latest add @nalarx/tabs

Usage

SegmentedTabs is the quick form: a list of tabs, the active key and a change handler. You render the content for activeKey yourself.

import { SegmentedTabs } from "@/components/ui/nx/Tabs";

<SegmentedTabs
  ariaLabel={t("automations.filter")}
  activeKey={filter}
  onChange={setFilter}
  tabs={[
    { key: "all", label: t("automations.all"), count: 14 },
    { key: "paused", label: t("automations.paused"), count: 5 },
    { key: "draft", label: t("automations.draft"), disabled: true },
  ]}
/>

The parts (Tabs, TabsList, TabsTrigger, TabsContent) add panels wired to their tabs (aria-controls, aria-labelledby):

<Tabs defaultValue="general">
  <TabsList variant="underline" aria-label="Settings sections">
    <TabsTrigger value="general">General</TabsTrigger>
    <TabsTrigger value="members">Members</TabsTrigger>
  </TabsList>
  <TabsContent value="general">…</TabsContent>
  <TabsContent value="members">…</TabsContent>
</Tabs>

Pill with counts, a disabled tab and a hidden one (Archived); underline with a right slot and a count that is still loading; a radio group with icons and a strong Edit mode, stretched with fill; the parts with panels:

Workspace name and language.
Workspace name and language.

Use pill for filters over one list and underline for page sections. When the strip picks a setting value instead of switching the view below it, pass semantics="radio": same look, radiogroup semantics. Add name to render native radio inputs, which work before the page hydrates:

<SegmentedTabs
  semantics="radio"
  name="theme"
  ariaLabel="Theme"
  activeKey={theme}
  onChange={setTheme}
  tabs={[
    { key: "light", label: "Light", icon: "sun" },
    { key: "dark", label: "Dark", icon: "moon" },
    { key: "system", label: "System", icon: "monitor" },
  ]}
/>

For other choices inside a form, Toggle group also works.

Keyboard

Base UI Tabs: Tab moves focus into the list onto the active tab and out again. ← and → move between tabs (wrapping at the ends), Home and End jump to the first and last. SegmentedTabs switches as focus moves. With the parts, focus alone does not switch: Enter or Space activates the focused tab (pass activateOnFocus to TabsList to switch on arrow keys). Disabled tabs can be focused but not activated.

Radio mode (semantics="radio"): the group is one Tab stop on the checked item. Arrow keys, Home and End move and select at once, wrapping and skipping disabled items. The rule is exported as rovingIndex(key, from, disabled). With name, the browser's own radio keys apply.

Props

SegmentedTabs:

PropTypeDefault
tabsSegmentedTabItem<K>[]See the item table below.
activeKeyK
onChange(key: K) => void
ariaLabelstring"Filter"The name of the tab list or radio group.
variant"pill" | "underline""pill"
semantics"tabs" | "radio""tabs"tabs switches the view below (tablist); radio picks a setting value (radiogroup).
namestringradio only: native radio inputs under this group name.
fillbooleanfalsepill only: the track spans its container and the tabs share the width.
rightSlotReactNodeAt the right end of the underline row. underline only.
containerClassNamestringOn the track (pill) or the hairline row (underline).

SegmentedTabItem:

FieldType
keyK
labelReactNode
iconIconNameDrawn at 14px before the label.
countnumber | string | nullAfter the label; a short ratio such as "3/11" works. null while it loads: a skeleton keeps the tab's width.
titlestringTooltip, e.g. why the tab is disabled.
descriptionstringRead by screen readers with the label.
emphasis"strong"pill only: an ink thumb while this tab is active, for a mode that changes things.
hiddenbooleanLeaves the tab out without removing it from the list.
disabledbooleanNever applies to the active tab.

Tabs takes every Base UI Tabs.Root prop (value, defaultValue, onValueChange). TabsList takes variant plus every Tabs.List prop (activateOnFocus, loopFocus). TabsTrigger needs value and takes disabled. TabsContent needs value and takes keepMounted.

Replaces

SegmentedTabs.tsx in web and the Platform (same API; the nx-seg-* classes in both globals.css are no longer needed by it), and the Platform's base/Tabs.tsx (Tabs, TabsList with variant="underline", TabsTrigger, TabsContent). The facades keep the names SegmentedTabs and SegmentedTabItem.

On this page