Tabs indicator
Free · MITTabs whose indicator glides between tabs on a shared-layout spring (underline with hover highlight, pill, or segmented with icons), with arrow-key navigation and panels that slide in the direction of travel while the height eases to fit.
Installation
$ npx shadcn@latest add https://goodui.dev/r/tabs-indicator.jsonThe file lands in components/goodui/tabs-indicator.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/tabs-indicator works too.
Usage
import { TabsIndicator } from "@/components/goodui/tabs-indicator"
export function BookingTabs({ upcoming, pending, past }: { upcoming: React.ReactNode; pending: React.ReactNode; past: React.ReactNode }) {
return (
<TabsIndicator
listLabel="Booking status"
defaultValue="upcoming"
items={[
{ value: "upcoming", label: "Upcoming", badge: 4, content: upcoming },
{ value: "pending", label: "Pending", badge: 2, content: pending },
{ value: "past", label: "Past", content: past },
{ value: "cancelled", label: "Cancelled", disabled: true },
]}
/>
)
}When to use
- Switching between views of the same data: Upcoming / Pending / Past bookings.
- Short tab sets where a gliding indicator and sliding panels make the change easy to follow.
- Range pickers above a chart (Day / Week / Month / Year) with the pill variant and your own content below.
- Choosing a setting or filter value without panels: use segmented-control (radiogroup semantics).
- Moving between pages of the app: use links in your nav, or bottom-nav on mobile.
- Needing vertical tabs, enclosed folder tabs or more variants: use the tabs core component.
| If you need | Use |
|---|---|
| Section tabs under a heading | variant="underline" (default, with hover highlight) |
| Chips-like range picker | variant="pill" |
| Compact switcher with icons | variant="segmented" with icon on items |
| Tabs fill the card width | fullWidth |
| Built-in animated panels | content on each item |
| You render the content yourself | no content; read value / onValueChange |
Examples
Controlled pill range picker with your own content
const [range, setRange] = React.useState("week")
<TabsIndicator
variant="pill"
listLabel="Revenue range"
value={range}
onValueChange={setRange}
items={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
{ value: "year", label: "Year" },
]}
/>
<RevenueChart range={range} />Segmented with icons and panels
<TabsIndicator
variant="segmented"
fullWidth
listLabel="Calendar view"
defaultValue="day"
items={[
{ value: "day", label: "Day", icon: CalendarDays, content: <DayView /> },
{ value: "week", label: "Week", icon: CalendarRange, content: <WeekView /> },
{ value: "month", label: "Month", icon: Calendar, content: <MonthView /> },
]}
/>API reference
| Prop | Type | Default |
|---|---|---|
| items* The tabs, in order. Give items content to get built-in panels. | TabsIndicatorItem[] | — |
| value Selected tab value (controlled). | string | — |
| defaultValue Initially selected tab when uncontrolled; defaults to the first enabled item. | string | — |
| onValueChange Called with the new value on click or arrow-key selection. | (value: string) => void | — |
| variant underline: line under the active tab plus a hover highlight. pill: filled primary pill. segmented: raised thumb in a muted track. | "underline" | "pill" | "segmented" | "underline" |
| fullWidth Stretch the tabs into equal columns. | boolean | false |
| listLabel Accessible name of the tab list. | string | — |
| listClassName Classes for the tablist element. | string | — |
| panelClassName Classes for the tab panel (only rendered when items have content). | string | — |
Also accepts every <div> attribute.
Accessibility
| Tab | Focuses the selected tab, then the panel (one tab stop for the list). |
| ArrowRight / ArrowLeft | Selects and focuses the next or previous enabled tab, wrapping around. |
| Home / End | Selects the first or last enabled tab. |
| Enter / Space | Selects the focused tab (native <button>). |
- Uses role="tablist" (aria-orientation horizontal, named by listLabel) with role="tab" buttons, aria-selected and a roving tabindex.
- With content, a role="tabpanel" is rendered, labelled by its tab and linked from the selected tab through aria-controls; the panel is focusable.
- Without content there is no tabpanel or aria-controls; if you render the panel yourself, add role="tabpanel" to it.
- Disabled items are skipped by the arrow keys.
Motion
- The indicator (underline, pill or thumb) is one shared-layout element that glides to the selected tab on a spring (stiffness 420, damping 36, mass 0.8).
- Underline variant: a muted highlight follows the pointer between tabs on the same spring and fades out in 150 ms when the pointer leaves.
- Panels slide 28px in the direction of travel with a 2px blur and fade (spring 380 / 34, opacity 180 ms) while the container height eases to the new panel (0.35 s, no bounce).
- Reduced motion: the indicator jumps, panels swap without sliding or blur, and the height changes instantly.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- Pass tabs as items={[{ value, label, icon?, badge?, disabled?, content? }]}; there are no Tab or TabPanel child components.
- icon takes the component (icon: CalendarDays), not an element.
- Use content for built-in panels; if any item has content, a panel is rendered for the selected one.
- Always pass listLabel so the tablist has a name.
- The list scrolls horizontally with a hidden scrollbar when tabs overflow; do not wrap it in your own overflow container.
FAQ
How is this different from the core tabs component?
tabs-indicator is a focused, items-based component with three variants and directional panels. The core tabs component has more variants (enclosed, soft, vertical), tones and sizes.
Can several instances live on one page?
Yes. The indicator's layoutId is scoped with useId, so indicators never jump between instances.
Related
Six variants (underline, pill, segmented, enclosed folder tabs, soft, vertical line), a sliding, fading or static indicator, fade or directional slide panels, tones, sizes, shapes, shadows and orientation, with icons, count badges, arrow keys and a one-line items API.
iOS-style segmented control whose thumb springs to the chosen segment and squeezes while pressed, with sizes, icons, full-width mode, dividers that hide next to the thumb, and radio-group arrow keys.
Mobile bottom bar where a pill glides to the selected tab and its icon bounces, in a labelled or expanding variant, with popping badges and an optional raised center action.
Horizontal or vertical stepper where the connecting line fills between steps, numbers morph into drawn checkmarks, a current-step ring glides to the active step, errors shake, and completed steps are clickable.