Skip to content

Switches between tabbed content views (shadcn-like strip; the co-located BbTabsRoot / BbTabsList / BbTabsPanes exports enable split layouts).

import { BbTabs } from 'bitboss-ui';

On this page

Props

NameTypeDefaultDescription
blockboolean | undefinedfalse

Displays the component as full width.

Full width with equal-width triggers, measured from the widest so they stay even once the strip overflows. Horizontal only. It replaces the full-width class most v2 projects wrote for themselves — delete yours, or the two fight over the same declarations with nothing to say which won.

See example
compactboolean | undefinedfalse

Adds a CSS class that applies a compact style to the component.

Drops the whole scale one notch — a 28px strip with smaller text and padding — so it lines up with compact fields.

See example
direction"horizontal" | "vertical" | undefined"horizontal"

Direction of the tabs component.

A vertical strip becomes a column beside the panes and answers Up/Down as well as Left/Right. It also reads a --max-w custom property to cap its width, which is the only knob that works from an ancestor.

See example
disableAnimateXboolean | undefinedfalse

Disables the horizontal slide transition between tab panels (enabled by default).

Inverted from v2's animateX, which defaulted to true. A leftover :animate-x="false" type-checks, falls into $attrs and lands on the root as a plain attribute, so the slide comes back; dev warns by name, production does not.

See example
disableAnimateYboolean | undefinedfalse

Disables the smooth height animation when switching between tab panels (enabled by default).

Inverted from v2's animateY, the same way as disableAnimateX. Reach for it when a pane hosts its own transitions or virtualized content that fights the height measurement.

See example
disabledboolean | undefinedfalse

Disables the component

Disables the whole group: every trigger goes disabled and keyboard navigation is inert. For a single unreachable tab, set disabled on its items entry instead — it stays visible, is skipped by the arrows, and can never be the default selection.

See example
eagerboolean | undefinedfalse

Prevents rendering content before it's made visible.

Mounts every pane from the first render and keeps it mounted, hidden while inactive; a lazy pane unmounts on every switch away and loses its state. Prefer the per-item form ({ key, label, eager: true }) when only one pane needs it — an eager pane costs its render on every page load. It never leaks to assistive technology: an inactive pane is display: none either way.

See example
headersRecord<string, string> | undefined-

Additional HTTP headers forwarded to Inertia when navigation is triggered. Only meaningful in an Inertia context.

idstring | undefined-

The identifier of the component.

Also the address for useBbTabsContext(id), which is how a banner or a page header outside the tabs steers them. Generated ids are not addressable, so pass one when anything outside the tree needs to reach the group.

See example
itemsrequiredBbTabsItem<K>[]-

Array of items that define the tabs in the component.

Each entry carries its own disabled, eager, href, server and Inertia options, and item values always win over the component-level ones. The key stays raw everywhere you write it — v-model, the item itself — but slot names, DOM ids and the URL all use the normalized slug: lowercased, with every run of non-word characters folded to _.

See example
modelValueK | null | undefined-

The current tab key. Used by v-model.

The raw item key, never the normalized slug. Leave it unbound (or null) and the first enabled tab is selected for you; if every tab is disabled the strip renders with no selection rather than throwing.

See example
navigationboolean | undefinedfalse

Synchronizes the current tab with the URL query parameter. Pair it with replace unless a tab is genuinely a separate destination — see the replace prop.

Makes the strip a writer of the URL query. Pair it with replace unless a tab is a destination of its own, and remember that a prerendered page is built with no query at all: the static HTML always carries the fallback tab, and the URL only takes over once the page is in the browser.

See example
onBefore(() => boolean | void) | undefined-

Inertia lifecycle hook — fired just before the request is sent. Return false to cancel the visit.

onCancel(() => void) | undefined-

Inertia lifecycle hook — fired when the request is cancelled.

onError((errors: Record<string, string>) => void) | undefined-

Inertia lifecycle hook — fired when the server returns validation errors.

onFinish(() => void) | undefined-

Inertia lifecycle hook — fired after the visit finishes (success or error).

onlystring[] | undefined-

Limits which server-side props are refreshed during a partial Inertia reload. Only meaningful in an Inertia context.

onProgress((progress: { percentage: number | undefined; }) => void) | undefined-

Inertia lifecycle hook — fired when upload/download progress is available.

onStart(() => void) | undefined-

Inertia lifecycle hook — fired when the request starts.

onSuccess(() => void) | undefined-

Inertia lifecycle hook — fired when the visit completes successfully.

preserveScrollboolean | ((props: Record<string, unknown>) => boolean) | undefined-

Controls whether Inertia preserves the scroll position after navigation. Only meaningful in an Inertia context.

preserveStateboolean | ((props: Record<string, unknown>) => boolean) | null | undefined-

Controls whether Inertia preserves the current component state after navigation. Only meaningful in an Inertia context.

ptPtItemMap<BbTabsPtPart, "item", BbTabsPtState, BbTabsItem<K>> | undefined-

Passthrough, object form: the same keys as the pt:* attributes without the prefix. See the pt:<part> row.

pt:itemPtValue | ((ctx: PtItemCtx<BbTabsItem<K>, BbTabsPtState>) => PtItemResult) | undefined-
pt:item:activePtValue | undefined-
pt:item:disabledPtValue | undefined-
pt:panelPtValue | undefined-
pt:panel:activePtValue | undefined-
pt:panel:disabledPtValue | undefined-
pt:rootPtValue | undefined-
pt:root:activePtValue | undefined-
pt:root:disabledPtValue | undefined-
queryKeystring | undefined"tab"

Defines the query key used for URL-based navigation.

Spelled querykey in v2. The all-lowercase form type-checks, falls through to $attrs and the sync silently moves to the default ?tab=. The value written is the normalized slug, not the key, so anything else reading the parameter — a server, an analytics filter — must compare against the slug.

See example
replaceboolean | undefinedfalse

Replaces history rather than pushing it when changing tab. Recommended default whenever navigation is on: the query keeps the active tab (deep-linkable, survives reload) while Back still returns to the page the user came from, instead of undoing one tab click at a time. Leave it off only when a tab is a destination of its own — typically a server visit whose content the user should be able to go Back to.

Effectively required alongside navigation: without it every tab click is a history entry, so Back walks the reader through their clicks instead of leaving the page.

See example
serverboolean | undefinedfalse

When true and in an Inertia context, triggers a real server-side Inertia visit instead of a client-only history.pushState.

variantkeyof TabsVariantRegistry | undefined"pill"

Look of the tab strip. pill is the segmented strip — a muted rounded bar with a raised pill sliding under the active trigger; line puts the triggers on a thin baseline with a sliding underline (on the side edge when direction="vertical"); none keeps behaviour and structure with no chrome, for fully custom designs through pt and classes. Every look is a preset of the strip's tokens (--list-bg, --indicator-bg, --indicator-h, --active-fg, …). Register more via the vite plugin tabsVariants option and style .bb-tabs-list--<variant> with the same tokens. The class lands on the strip (BbTabsList), which can also take its own variant on the split layout.

v-model

Values kept in sync through v-model.

NameTypeDescription
modelValueK | null | undefinedSee example

Events

NameTypeDescription
update:modelValue(e: "update:modelValue", value: K): void

Emitted when the selected tab changes. Used by v-model.

The whole event surface. It also fires once on mount when the component resolves a key the parent never set — from the URL query or the first-enabled fallback — so a binding starts aligned.

See example

Slots

The listed properties are the ones exposed to the slot scope.

NameTypeDescription
<key>BbTabsSlotProps<K>

One tab's content pane, by the tab key through slotKey; no fallback (an empty pane).

The pane. Its name is the normalized key (billing-info is #billing_info); with items generated, write #[slotKey(item.key)] rather than restating the rule. Only the active pane is mounted unless the pane is eager.

See example
header:appendBbTabsScrollSlotProps

Content rendered after the scrollable tab label list (e.g. a right scroll arrow).

The trailing counterpart of header:prepend. Bind each arrow to canScrollLeft / canScrollRight so it dims at the ends of the strip.

See example
header:prependBbTabsScrollSlotProps

Content rendered before the scrollable tab label list (e.g. a left scroll arrow).

Rendered outside the scrollable list, so it stays put while the strip moves. The buttons are your markup, which means an icon-only arrow needs an aria-label you write.

See example
labelBbTabsSlotProps<K>

Fallback slot used for all tab labels when no per-tab label:<key> slot is provided.

The fallback for every trigger; a label:<key> slot beats it for that one tab. Keep it presentational: the trigger is already the control, so a nested button or link is invalid and steals the click.

See example
label:<key>BbTabsSlotProps<K>

One tab trigger's label, by the tab key through slotKey; beats label, which beats the item's label text.

The key is normalized: inReview is #label:inreview, In Review is #label:in_review. Presentation only, like label.

See example

Changes from v2

Props removed or renamed in 3.0. If you are coming from v2, the answer is here.

  • animateXdisableAnimateXfails silently
  • animateYdisableAnimateYfails silently
  • querykeyqueryKeyfails silently