tab

Tabs

Switch between related views in-place.

Tabs Examples

Copy-ready examples for Tab + TabStack.

Tab (header only)

Use Tab alone and render panels conditionally below it.

Live preview

Overview content

Basic header only

"use client";

import { useState } from "react";
import Tab from "@/packages/ui/src/_components/tab/Tab";

export default function Example() {
  const [active, setActive] = useState(0);

  return (
    <div>
      <Tab
        activeTabIndex={active}
        onActiveTabChange={setActive}
        tabOption={["Overview", "Details", "Reviews"]}
      />

      {active === 0 ? <div>Overview content</div> : null}
      {active === 1 ? <div>Details content</div> : null}
      {active === 2 ? <div>Reviews content</div> : null}
    </div>
  );
}

Tab + TabStack (slide panels)

Minimal sliding panels without gesture bindings.

Use `stackStyle.transform` to slide the track as the active index changes.

Live preview

Content: Photos
Content: Favorites
Content: Nearby

Sliding panels

"use client";

import { useMemo, useRef, useState } from "react";
import Tab from "@/packages/ui/src/_components/tab/Tab";
import TabStack from "@/packages/ui/src/_components/tab/tab_stack/Tab_stack";

export default function Example() {
  const [active, setActive] = useState(1);

  const stackRef = useRef<HTMLDivElement>(null);
  const stackBind = {};

  const stackStyle = useMemo(
    () => ({
      transform: `translateX(-${active * 100}%)`,
      transition: "transform 220ms ease",
    }),
    [active]
  );

  return (
    <>
      <Tab
        activeTabIndex={active}
        onActiveTabChange={setActive}
        tabOption={["Photos", "Favorites", "Nearby"]}
      />

      <TabStack stackRef={stackRef} stackBind={stackBind} stackStyle={stackStyle}>
        {[
          <div key="p1">Content: Photos</div>,
          <div key="p2">Content: Favorites</div>,
          <div key="p3">Content: Nearby</div>,
        ]}
      </TabStack>
    </>
  );
}

useTabsIndicator (recommended pairing)

Use the indicator hook to bind slide behavior + keep measurements in sync.

Live preview

Content: Photos
Content: Favorites
Content: Nearby

Hook + TabStack

"use client";

import { useState } from "react";
import Tab from "@/_components/tab/Tab";
import Badges from "@/_components/badges/Badges";
import TabStack from "@/_components/tab/tab_stack/Tab_stack";
import { useTabsIndicator } from "@/_components/tab/hooks/useTabIndicator";

export default function Example() {
  const [active, setActive] = useState(1);

  const tabs = useTabsIndicator({
    padX: 12,
    activeIndex: active,
    count: 3,
    onChangeIndex: setActive,
  });

  return (
    <>
      <Tab
        activeTabIndex={active}
        onActiveTabChange={setActive}
        tabOption={[
          <Badges
            key="photos"
            iconName="Home"
            label="Photos"
            showInfo
            baseNumber={10}
            baseLimit={999}
            size="small"
            orientation="vertical"
            labelFlow="before"
          />,
          <Badges
            key="fav"
            iconName="favorite"
            label="Favorites"
            showInfo
            baseNumber={3}
            baseLimit={999}
            size="small"
            orientation="vertical"
            labelFlow="before"
          />,
          <Badges
            key="near"
            iconName="place"
            label="Nearby"
            showInfo={false}
            baseNumber={0}
            baseLimit={999}
            size="small"
            orientation="vertical"
            labelFlow="before"
          />,
        ]}
      />

      <TabStack
        stackRef={tabs.stackRef}
        stackBind={tabs.stackBind}
        stackStyle={tabs.stackStyle}
      >
        {[<p key="c1">Content: Photos</p>, <p key="c2">Content: Favorites</p>, <p key="c3">Content: Nearby</p>]}
      </TabStack>
    </>
  );
}

Custom tab nodes

tabOption accepts any ReactNode. Great for icons, badges, counters, etc.

Live preview

Active index: 0

Custom nodes

"use client";

import { useMemo, useState } from "react";
import Tab from "@/packages/ui/src/_components/tab/Tab";

export default function Example() {
  const [active, setActive] = useState(0);

  const tabOption = useMemo(
    () => [
      <div key="t1" style={{ display: "flex", gap: 8, alignItems: "center" }}>
        <span style={{ fontWeight: 700 }}>Account</span>
        <span style={{ opacity: 0.65, fontSize: 12 }}>New</span>
      </div>,
      <div key="t2" style={{ display: "flex", gap: 8, alignItems: "center" }}>
        <span style={{ fontWeight: 700 }}>Billing</span>
        <span style={{ opacity: 0.65, fontSize: 12 }}>2 issues</span>
      </div>,
    ],
    []
  );

  return <Tab activeTabIndex={active} onActiveTabChange={setActive} tabOption={tabOption} />;
}

Many tabs (scrollable header)

Wrap Tab with `overflowX: auto` to avoid breaking the layout.

Live preview

Selected tab: 1

Scrollable header

"use client";

import { useMemo, useState } from "react";
import Tab from "@/packages/ui/src/_components/tab/Tab";

export default function Example() {
  const [active, setActive] = useState(0);

  const tabs = useMemo(
    () => Array.from({ length: 10 }).map((_, i) => <span key={i}>Tab {i + 1}</span>),
    []
  );

  return (
    <div style={{ overflowX: "auto", width: "100%" }}>
      <Tab activeTabIndex={active} onActiveTabChange={setActive} tabOption={tabs} />
    </div>
  );
}

Controlled from parent

External UI can set the active tab index.

Live preview

This is a fully controlled pattern. External actions can set the active tab.

Fully controlled

"use client";

import { useState } from "react";
import Tab from "@/_components/tab/Tab";

export default function Example() {
  const [active, setActive] = useState(1);

  return (
    <>
      <button type="button" onClick={() => setActive(0)}>Go to Overview</button>
      <button type="button" onClick={() => setActive(2)}>Go to Reviews</button>

      <Tab
        activeTabIndex={active}
        onActiveTabChange={setActive}
        tabOption={["Overview", "Details", "Reviews"]}
      />
    </>
  );
}

On this page

Tabs Props

Tabs — Props
PropTypeRequiredDefaultDescription
Tab.activeTabIndexnumberYesActive tab index (0-based).
Tab.tabOptionReact.ReactNode[]YesTab items rendered in the header (strings, icons, badges, etc.).
Tab.onActiveTabChange(index: number) => voidNoCalled when a tab is clicked. Recommended for controlled usage.
TabStack.stackRefReact.RefObject<HTMLDivElement | null>YesRef for the viewport element (useful for measurements/gestures).
TabStack.stackBindRecord<string, any>YesEvent bindings spread onto the viewport (e.g., pointer/gesture handlers).
TabStack.stackStyleReact.CSSPropertiesYesInline styles applied to the inner track (commonly a `transform`).
TabStack.childrenReact.ReactNode[]YesPanels. Each child becomes one slide (order must match `tabOption`).
TabStack.styleReact.CSSPropertiesNoExtra inline styles applied to the viewport.
useTabsIndicator(options).padXnumberNo0Horizontal padding used for indicator/measurement calculations.
useTabsIndicator(options).activeIndexnumberYesCurrent active index (same value you pass to Tab/TabStack).
useTabsIndicator(options).countnumberYesNumber of tabs/panels.
useTabsIndicator(options).onChangeIndex(nextIndex: number) => voidYesSetter called by the hook when user interaction changes the index.