component

Tab

A complete, accessible tab component system for organizing content into separate views. Built with keyboard navigation support, badge indicators, and horizontal scrolling for many tabs.

Default

The default Tab component combines all subcomponents into a convenient wrapper. Use snippets (Svelte) or named slots (Astro) to define content for each tab.

Dashboard

Welcome to your dashboard! Here's an overview of your account.

---
import Tab from "../tab.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "dashboard", label: "Dashboard" },
  { id: "profile", label: "Profile" },
  { id: "notifications", label: "Notifications" },
  { id: "settings", label: "Settings" },
];
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <Tab items={tabItems} defaultTab="dashboard">
    <div slot="dashboard">
      <h3>Dashboard</h3>
      <p>Welcome to your dashboard! Here's an overview of your account.</p>
    </div>

    <div slot="profile">
      <h3>Profile</h3>
      <p>Update your profile information and preferences here.</p>
    </div>

    <div slot="notifications">
      <h3>Notifications</h3>
      <p>Manage your notification settings and view recent alerts.</p>
    </div>

    <div slot="settings">
      <h3>Settings</h3>
      <p>Customize your application settings and preferences.</p>
    </div>
  </Tab>
</div>

Appearance

The strip renders segmented buttons by default. appearance="tabs" switches it to the minimal underline tab strip — transparent tabs, a 2px accent bar under the selected one, and a 1px baseline track. The bar is drawn inside each tab, so the underline look scrolls cleanly when the strip overflows; it’s the same visual language as Navigation’s appearance="tabs" and Radio’s appearance="tab".

The underline tab strip — appearance="tabs".

Underline tabs scroll horizontally when the strip overflows.

---
import Tab from "../tab.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "overview", label: "Overview" },
  { id: "activity", label: "Activity" },
  { id: "settings", label: "Settings" },
];

// Enough tabs to overflow the strip — the underline look scrolls cleanly: the
// 2px bar is inset in each tab and the baseline track spans the full
// scrollable width.
const manyItems = [
  { id: "analytics", label: "Analytics" },
  { id: "reports", label: "Reports" },
  { id: "customers", label: "Customers" },
  { id: "products", label: "Products" },
  { id: "orders", label: "Orders" },
  { id: "inventory", label: "Inventory" },
  { id: "shipping", label: "Shipping" },
  { id: "returns", label: "Returns" },
  { id: "preferences", label: "Preferences" },
];
---

<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
  <Tab items={tabItems} defaultTab="overview" appearance="tabs">
    <p slot="overview">
      The underline tab strip — <code>appearance="tabs"</code>.
    </p>
    <p slot="activity">Recent activity across your account.</p>
    <p slot="settings">Customize your application settings.</p>
  </Tab>

  <Tab
    items={manyItems}
    defaultTab="analytics"
    appearance="tabs"
    fullWidth={false}
  >
    <p slot="analytics">
      Underline tabs scroll horizontally when the strip overflows.
    </p>
    <p slot="reports">Generate and export monthly reports.</p>
    <p slot="customers">Customer accounts and segments.</p>
    <p slot="products">Manage the product catalog.</p>
    <p slot="orders">Track and fulfill orders.</p>
    <p slot="inventory">Stock levels and warehouses.</p>
    <p slot="shipping">Shipping options and carriers.</p>
    <p slot="returns">Process customer returns.</p>
    <p slot="preferences">Store preferences.</p>
  </Tab>
</div>

Composable

For more control, use the individual components: TabGroup, TabNav, and TabPanel. This allows you to customize the layout and access the selected tab state.

This is the Home tab content. You can build custom layouts here.

---
import TabGroup from "../tab-group.astro";
import TabNav from "../tab-nav.astro";
import TabPanel from "../tab-panel.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabs = [
  { id: "home", label: "Home" },
  { id: "about", label: "About" },
  { id: "contact", label: "Contact" },
];
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <TabGroup defaultTab="home">
    <TabNav slot="tab-nav" tabs={tabs} selectedTab="home" />

    <div slot="panel">
      <TabPanel id="home" selected>
        <p>This is the Home tab content. You can build custom layouts here.</p>
      </TabPanel>

      <TabPanel id="about">
        <p>This is the About tab content with composable components.</p>
      </TabPanel>

      <TabPanel id="contact">
        <p>Contact us at: contact@example.com</p>
      </TabPanel>
    </div>
  </TabGroup>
</div>

With Badges

Add badge indicators to tabs using the badge property on tab items. Useful for showing counts like unread messages or pending items.

Inbox

You have 12 unread messages in your inbox.

---
import Tab from "../tab.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "inbox", label: "Inbox", badge: "12" },
  { id: "drafts", label: "Drafts", badge: "3" },
  { id: "sent", label: "Sent" },
  { id: "archive", label: "Archive" },
];
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <Tab items={tabItems} defaultTab="inbox">
    <div slot="inbox">
      <h3>Inbox</h3>
      <p>You have 12 unread messages in your inbox.</p>
    </div>

    <div slot="drafts">
      <h3>Drafts</h3>
      <p>You have 3 draft messages waiting to be sent.</p>
    </div>

    <div slot="sent">
      <h3>Sent</h3>
      <p>View all messages you've sent.</p>
    </div>

    <div slot="archive">
      <h3>Archive</h3>
      <p>Access your archived messages.</p>
    </div>
  </Tab>
</div>

Disabled Tabs

Individual tabs can be disabled by setting disabled: true on the tab item. Disabled tabs are not focusable and cannot be selected.

Available Items

These items are currently available and ready for action.

---
import Tab from "../tab.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "available", label: "Available" },
  { id: "processing", label: "Processing", disabled: true },
  { id: "completed", label: "Completed" },
  { id: "archived", label: "Archived", disabled: true },
];
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <Tab items={tabItems} defaultTab="available">
    <div slot="available">
      <h3>Available Items</h3>
      <p>These items are currently available and ready for action.</p>
    </div>

    <div slot="processing">
      <h3>Processing</h3>
      <p>Items currently being processed (This tab is disabled).</p>
    </div>

    <div slot="completed">
      <h3>Completed Items</h3>
      <p>All completed items are listed here.</p>
    </div>

    <div slot="archived">
      <h3>Archived</h3>
      <p>Archived items (This tab is disabled).</p>
    </div>
  </Tab>
</div>

Scrolling Navigation

When there are more tabs than can fit in the available width, the navigation scrolls horizontally. A thin styled scrollbar appears to indicate overflow. Focus outlines and group borders remain fully visible during scrolling.

Overview

A high-level summary of your store's performance and key metrics.

---
import Tab from "../tab.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "overview", label: "Overview" },
  { id: "analytics", label: "Analytics" },
  { id: "reports", label: "Reports" },
  { id: "customers", label: "Customers" },
  { id: "products", label: "Products" },
  { id: "orders", label: "Orders" },
  { id: "inventory", label: "Inventory" },
  { id: "shipping", label: "Shipping" },
  { id: "returns", label: "Returns" },
  { id: "preferences", label: "Preferences" },
];
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <Tab items={tabItems} defaultTab="overview">
    <div slot="overview">
      <h3>Overview</h3>
      <p>A high-level summary of your store's performance and key metrics.</p>
    </div>

    <div slot="analytics">
      <h3>Analytics</h3>
      <p>Detailed traffic and conversion analytics for your store.</p>
    </div>

    <div slot="reports">
      <h3>Reports</h3>
      <p>Generate and download reports for accounting and compliance.</p>
    </div>

    <div slot="customers">
      <h3>Customers</h3>
      <p>Manage customer profiles, segments, and communication preferences.</p>
    </div>

    <div slot="products">
      <h3>Products</h3>
      <p>Add, edit, and organize your product catalog.</p>
    </div>

    <div slot="orders">
      <h3>Orders</h3>
      <p>View and process incoming orders and track fulfillment status.</p>
    </div>

    <div slot="inventory">
      <h3>Inventory</h3>
      <p>Monitor stock levels and set up reorder alerts.</p>
    </div>

    <div slot="shipping">
      <h3>Shipping</h3>
      <p>Configure shipping zones, rates, and carrier integrations.</p>
    </div>

    <div slot="returns">
      <h3>Returns</h3>
      <p>Process return requests and manage refund policies.</p>
    </div>

    <div slot="preferences">
      <h3>Preferences</h3>
      <p>Configure store preferences, payments, and integrations.</p>
    </div>
  </Tab>
</div>

With Action Slots

Use leftAction and rightAction snippets to place fixed elements on either side of the scrollable tab strip. The action slots stay anchored while the tabs scroll freely behind them. Use fullWidth={false} to let tabs use their natural width and trigger scrolling.

Overview

A high-level summary of your store's performance and key metrics.

---
import { css } from "@pindoba/styled-system/css";
import TabGroup from "../tab-group.astro";
import TabNav from "../tab-nav.astro";
import TabPanel from "../tab-panel.astro";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "overview", label: "Overview" },
  { id: "analytics", label: "Analytics" },
  { id: "reports", label: "Reports" },
  { id: "customers", label: "Customers" },
  { id: "products", label: "Products" },
  { id: "orders", label: "Orders" },
  { id: "inventory", label: "Inventory" },
  { id: "shipping", label: "Shipping" },
  { id: "returns", label: "Returns" },
  { id: "preferences", label: "Preferences" },
];
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <TabGroup defaultTab="overview">
    <TabNav
      slot="tab-nav"
      tabs={tabItems}
      selectedTab="overview"
      fullWidth={false}
    >
      <Button slot="left-action" emphasis="secondary" aria-label="Scroll left"
        >Action 1</Button
      >
      <div
        class={css({
          display: "flex",
          gap: "xs",
        })}
        slot="right-action"
      >
        <Button emphasis="secondary" shape="square" aria-label="Add tab"
          >+</Button
        >
        <Button slot="right-action" emphasis="secondary" aria-label="Add tab"
          >Action 2</Button
        >
      </div>
    </TabNav>
    <div slot="panel">
      <TabPanel id="overview" selected>
        <div>
          <h3>Overview</h3>
          <p>
            A high-level summary of your store's performance and key metrics.
          </p>
        </div>
      </TabPanel>
      <TabPanel id="analytics">
        <div>
          <h3>Analytics</h3>
          <p>Detailed traffic and conversion analytics for your store.</p>
        </div>
      </TabPanel>
      <TabPanel id="reports">
        <div>
          <h3>Reports</h3>
          <p>Generate and download reports for accounting and compliance.</p>
        </div>
      </TabPanel>
      <TabPanel id="customers">
        <div>
          <h3>Customers</h3>
          <p>
            Manage customer profiles, segments, and communication preferences.
          </p>
        </div>
      </TabPanel>
      <TabPanel id="products">
        <div>
          <h3>Products</h3>
          <p>Add, edit, and organize your product catalog.</p>
        </div>
      </TabPanel>
      <TabPanel id="orders">
        <div>
          <h3>Orders</h3>
          <p>View and process incoming orders and track fulfillment status.</p>
        </div>
      </TabPanel>
      <TabPanel id="inventory">
        <div>
          <h3>Inventory</h3>
          <p>Monitor stock levels and set up reorder alerts.</p>
        </div>
      </TabPanel>
      <TabPanel id="shipping">
        <div>
          <h3>Shipping</h3>
          <p>Configure shipping zones, rates, and carrier integrations.</p>
        </div>
      </TabPanel>
      <TabPanel id="returns">
        <div>
          <h3>Returns</h3>
          <p>Process return requests and manage refund policies.</p>
        </div>
      </TabPanel>
      <TabPanel id="preferences">
        <div>
          <h3>Preferences</h3>
          <p>Configure store preferences, payments, and integrations.</p>
        </div>
      </TabPanel>
    </div>
  </TabGroup>
</div>

Custom Styling

Use the passThrough prop to customize styles for any tab slot — root, nav, navScroll, panel, panelsContainer, and panelEmpty. Each key accepts a Panda CSS SystemStyleObject.

Summary

This tab component uses custom styling via the passThrough prop.

---
import Tab from "../tab.astro";
import { stack } from "@pindoba/styled-system/patterns";

const tabItems = [
  { id: "summary", label: "Summary" },
  { id: "metrics", label: "Metrics" },
  { id: "exports", label: "Exports" },
];

const customStyles = {
  root: {
    style: {
      border: "2px solid",
      borderColor: "accent.500",
      borderRadius: "lg",
      padding: "4",
      bg: "neutral.50",
      _dark: {
        bg: "neutral.900",
      },
    },
  },
  nav: {
    style: {
      mb: "4",
    },
  },
  panel: {
    style: {
      padding: "6",
      bg: "white",
      borderRadius: "md",
      boxShadow: "sm",
      _dark: {
        bg: "neutral.800",
      },
    },
  },
  panelsContainer: {
    style: {
      mt: "4",
    },
  },
};
---

<div
  class={stack({
    gap: "xl",
    direction: "column",
    justify: "center",
    width: "100%",
  })}
>
  <Tab items={tabItems} defaultTab="summary" passThrough={customStyles}>
    <div slot="summary">
      <h3>Summary</h3>
      <p>This tab component uses custom styling via the passThrough prop.</p>
    </div>

    <div slot="metrics">
      <h3>Metrics</h3>
      <p>View your metrics data with custom styled panels.</p>
    </div>

    <div slot="exports">
      <h3>Exports</h3>
      <p>Generate and view exports with enhanced styling.</p>
    </div>
  </Tab>
</div>

Accessibility

The tab component includes full keyboard navigation support:

  • Tab: Move focus to the active tab or first tab
  • Arrow Keys: Navigate between tabs (handled by the Radio component)
  • Home: Jump to the first tab
  • End: Jump to the last tab
  • Space/Enter: Activate the focused tab (via Radio)

All ARIA attributes are properly set for screen reader compatibility.

props · 17 shown · 17 total
TabGroup
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Background treatment of the tab group surface.

border
"none""bold""default""muted""accent"

Border style of the tab group surface.

emphasis
"primary""secondary""tertiary"

Visual emphasis level of the tab group surface.

feedback
"primary""neutral""success""warning""danger""inherit"

Feedback color applied to the tab group surface.

interactive
boolean

Whether the tab group surface responds to hover/interaction states.

padding
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""7xl""8xl""4xs""3xs""2xs""9xl""10xl""11xl"

Inner padding of the tab group surface.

passThrough
TabPassThrough<RootElementAttributes>

Per-slot style and attribute override bag for the tab group's slots.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius of the tab group surface.

translucent
boolean

Whether the tab group surface uses a translucent background.

TabNav
actionsDisplay
"responsive""popover""inline"
default responsive

How the action slots are displayed relative to the tab strip.

appearance
"buttons""tabs"
default "buttons"

Visual treatment of the tab strip. `"buttons"` renders each tab as a segmented button (Radio `appearance="button"`); `"tabs"` renders the minimal underline tab strip (Radio `appearance="tab"` — transparent tabs, a 2px accent bar under the selected one, and a baseline track).

fullWidth
boolean
default true

Whether tabs should take full width.

passThrough
TabPassThrough

Per-slot style and attribute override bag for the nav slots.

tabs required
readonly TItem[]

Array of tab navigation items with id and label (plus per-tab extras).

TabPanel
ariaLabelledBy
string

ID of the element labeling this panel (defaults to the tab's id).

id required
string

Unique identifier matching a tab ID — the panel's HTML id will be `panel-${id}`.

passThrough
TabPassThrough

Per-slot style and attribute override bag for the panel slots.

Type

  • Components
  • Blocks