ComponentsNavigation

Tabs

A list of tabs that shows one panel at a time. Use it for sections of one page, views inside a panel, and the steps of a setup flow.

Tabs playground

Fictional project tabs. The third tab is disabled and has no panel.

Fictional project overview.

Usage

Tabs usageTSX
import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from "@tulipfarm/sprout/tabs";

<Tabs defaultValue="members">
  <TabsList aria-label="Members settings">
    <TabsTrigger value="members">Members (12)</TabsTrigger>
    <TabsTrigger value="invitations">Invitations</TabsTrigger>
  </TabsList>
  <TabsContent value="members">…</TabsContent>
  <TabsContent value="invitations">…</TabsContent>
</Tabs>

Tabs holds the active value. Pass defaultValue to let it manage itself, or value and onValueChange to control it from state or the address. Each TabsTrigger value matches one TabsContent. Give the list an aria-label that names the group.

Variants

default is a filled segmented control for a few settings groups.line underlines the active tab for page sections and views inside cards and popovers. steps fills the width for a numbered setup flow and uses the primary color, so the TulipFarm theme makes it green.

default
line
steps

Settings groups

The default list fits short labels and counts. keepMounted keeps both panels in the page, so a list keeps its place and its pending changes.

Members settings

Default tabs with a count in the label. Both panels stay mounted.

  • Priya Ramanpriya@meadow.exampleAdmin
  • Alex Meadowalex@meadow.exampleMember
  • Robin Fieldrobin@meadow.exampleMember

Page sections

Tabs from the addressTSX
<Tabs
  value={tab}
  onValueChange={(value) =>
    navigate({ search: { tab: value === "access" ? undefined : value } })
  }
>
  <div className="max-w-full overflow-x-auto pb-1">
    <TabsList variant="line" aria-label="Model settings">…</TabsList>
  </div>
  <TabsContent value="access" keepMounted>…</TabsContent>
</Tabs>

Wrap a long line list in a horizontal scroller so it never widens the page. The underline and focus ring stay inside the list. Panels can show loading, empty, and error states with StatePanel, or render nothing until their data is ready.

Model settings

Line tabs read and write the address. Compaction stays empty until access loads.

/meadow/models
  • ClaudeResearch, SupportAvailable
  • GPTAll teamsAvailable
  • GeminiNo teams yetFallback: Claude

Agent page

Line tabs filtered by the person's capabilities. The list scrolls sideways on small screens.

Check setup, then start a task with this agent.

Weekly report writer is ready. Start a private task to try it.

Setup steps

Steps with markersTSX
<TabsList variant="steps" aria-label="Agent setup sections">
  {steps.map((step, index) => (
    <TabsTrigger key={step.id} value={step.id}>
      <span aria-hidden="true" className="grid size-5 place-items-center">
        {errors.has(step.id) ? <AlertCircle /> : done.has(step.id) ? <Check /> : index + 1}
      </span>
      {step.label}
      {errors.has(step.id) && <span className="sr-only">, needs attention</span>}
    </TabsTrigger>
  ))}
</TabsList>

Put a number, a check, or a warning icon before each label, and add hidden text for the warning. Steps need room, so swap them for a select on small screens. The preview card uses a line list, and its Edit buttons move the setup to a step.

Agent setup

Steps tabs mark finished and invalid sections. Below 640px a select replaces them. Panels stay mounted, so drafts survive.

N

Your new agent

Personal agent
Draft
  • SkillsNo skills selected
  • ActionsNo actions selected
  • Team placementPersonal agent
  • SlackNot attached
  • SchedulesManual use only

Nothing runs until you review and activate.

Inside a popover

Put a line list in a bordered header inside a panel popover. Leave out a trigger and its panel when an option does not apply, and fall back to another value. Without keepMounted, a panel unmounts when it is hidden.

Table search

Line tabs inside a panel popover. Tables without text fields hide the Text tab.

API

The four parts wrap Base UI Tabs. Every Base UI prop works, including orientation, disabled on a trigger, activateOnFocus and loopFocus on the list, keepMounted on a panel, render, andclassName as a string or a state function.

Parts get data-slot values of tabs, tabs-list, tabs-trigger, and tabs-content. The list has data-variant, every part has data-orientation, and the active trigger has data-active. Mark trigger icons with data-icon="inline-start" or "inline-end" to tighten padding. tabsListVariants() returns list classes and tabsListVariantNames lists the variants.

Accessibility

The list is a tablist, each trigger is a tab witharia-selected, and each panel is a tabpanel labelled by its tab. Tab moves into the list and then to the panel. Arrow keys move between tabs and skip disabled ones, Home and End jump to the ends, and Enter or Space selects. Vertical lists use the up and down arrows.

Colors and the underline change over 120–180ms. Both stop when reduced motion is requested.

Theme tokens

Tabs use --sprout-secondary, --sprout-surface,--sprout-text, --sprout-muted, --sprout-line, --sprout-input, --sprout-primary, --sprout-focus, --sprout-radius-control, --sprout-shadow-control, and the motion tokens. They follow light, dark, and the TulipFarm theme.