Tabs

Tabs for switching between views within a surface, in three shapes over one mechanism. CSS cannot measure DOM geometry, so the active trigger's box is read in a layout effect and written onto the list as --indicator-x, -y, -w and -h; the indicator draws itself entirely from those four numbers, which is why its position and its width travel on a single transition. A MutationObserver re-measures when the active tab changes, a ResizeObserver catches reflow, and the first placement is flushed before the transition class is attached so the indicator lands on the active tab instead of gliding in from the corner.

tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@pestoui/core"

Default

Agent summary, connected apps and recent activity.

Pill

The default track. Click across the row: the indicator's x offset and its width interpolate together on one transition, so the raised surface stretches and shrinks as it travels. Labels of deliberately different lengths make the width half of that visible — with equal-width tabs you would only ever see the slide.

Three agents running.

Underline

The track goes away and the indicator becomes a thin bar on the active trigger's bottom edge. The measurement is unchanged — the bar still reads the same four custom properties, it just substitutes a fixed thickness for the measured height and rides the trigger's far edge. Travel and width interpolation are identical to the pill.

Six events this hour.

Segment

The filled track squared back to the surface radius, with a tighter inset — a segmented control rather than a pill. Same travel, same width interpolation; only the corner and the padding change, and the triggers follow the container's corner so the focus ring traces the shape the indicator does.

2,140 runs.

Why the indicator overshoots

Almost every travelling indicator in the system uses ease-spring-glide, which is overdamped on purpose: an element that arrives at a known destination and then bounces past it reads as imprecise. The tabs indicator is the one deliberate exception. It moves between two tabs the reader just chose, both of them still on screen, so a small overshoot reads as momentum rather than error — and the longer settle gives the selection some life instead of letting it snap into place. Click any tab: both rows travel at once, the top on the shipped overshoot, the bottom overridden to glide.

ease-spring-overshoot — ζ 0.84, crosses a little past the tab and settles back

Ran 4 minutes ago.

ease-spring-glide — ζ 1.09, overdamped, so it never crosses the tab at all

Ran 4 minutes ago.

Props

PropTypeDefaultDescription
TabsList variant"pill" | "underline" | "segment""pill"Shape of the track and the indicator. pill and segment keep a filled track and raise a surface under the active tab; underline drops the track and draws a bar on the active tab's edge.
Tabs orientation"horizontal" | "vertical""horizontal"Vertical stacks the triggers and moves the underline bar to the trailing edge. The same measurement drives both — only which axis carries the fixed thickness changes.
Tabs defaultValuestring—Starting tab when the component manages its own state.
Tabs valuestring—Controlled active tab. Pair with onValueChange.
Tabs onValueChange(value: string) => void—Fires with the newly selected tab's value.
TabsTrigger valuestring—Required. Matches the value on the TabsContent this trigger reveals.
TabsContent valuestring—Required. Panels stay mounted and hidden, so switching plays an enter only — a short rise that lands as the indicator arrives.

Pesto UI · 117 documented components