Skip to content

Switches between views of one page from a row of tabs. For the tabs and their panels in different places of the layout, use BbTabsRoot with BbTabsList and BbTabsPanels.

import { BbTabs } from 'bitboss-ui';

On this page

Props

NameDescription
block Type: booleanDefaultfalse

Displays the component as full width.

Full width with equal-width triggers, measured from the widest so they stay even once the strip overflows. On a vertical strip it stretches every trigger to the column width instead. 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
compact Type: booleanDefaultfalse

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 Type: "horizontal" | "vertical"Default"horizontal"

Direction of the tabs component.

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

See example
disableAnimateX Type: booleanDefaultfalse

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
disableAnimateY Type: booleanDefaultfalse

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 panel hosts its own transitions or virtualized content that fights the height measurement.

See example
disabled Type: booleanDefaultfalse

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
eager Type: booleanDefaultfalse

Renders every tab's panel at mount and keeps it mounted, hidden while inactive, instead of only the active one. One tab alone: its item's eager.

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

See example
headers Type: Record<string, string>

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

id Type: string

The root's id, and the key useBbTabsContext(id) pairs by. Generated when omitted.

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
items requiredType: BbTabsItem<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 value stays raw everywhere you write it (v-model, the item itself), but slot names, DOM ids and the URL all use slotKey(value), the kebab-case slug: inReview and In Review are both in-review.

See example
manualActivation Type: booleanDefaultundefined (on when server is set on the strip or an item)

Arrow keys (and Home / End) only move focus; Enter or Space opens the tab (APG manual activation, Radix activationMode="manual"). Unset: on when the strip or an item has server (else every arrow press is a visit), off elsewhere; href alone does not turn it on.

modelValue Type: K | null

The active tab's value. Used by v-model.

The raw item value, never the 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
navigation Type: booleanDefaultfalse

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 Type: (() => boolean | void)

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

onCancel Type: (() => void)

Inertia lifecycle hook — fired when the request is cancelled.

onError Type: ((errors: Record<string, string>) => void)

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

onFinish Type: (() => void)

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

only Type: string[]

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

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

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

onStart Type: (() => void)

Inertia lifecycle hook — fired when the request starts.

onSuccess Type: (() => void)

Inertia lifecycle hook — fired when the visit completes successfully.

preserveScroll Type: boolean | ((props: Record<string, unknown>) => boolean)

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

preserveState Type: boolean | ((props: Record<string, unknown>) => boolean) | null

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

pt Type: ({ root?: PtValue | undefined; panel?: PtValue | undefined; "root:disabled"?: PtValue | undefined; "root:active"?: PtValue | undefined; "item:disabled"?: PtValue | undefined; "item:active"?: PtValue | undefined; list?: PtValue | undefined; indicator?: PtValue | undefined; "list:disabled"?: PtValue | undefined; "indicator:disabled"?: PtValue | undefined; "item:pill"?: PtValue | undefined; "item:line"?: PtValue | undefined; "panel:disabled"?: PtValue | undefined; "panel:active"?: PtValue | undefined; wrapper?: PtValue | undefined; panels?: PtValue | undefined; "list:active"?: PtValue | undefined; "indicator:active"?: PtValue | undefined; "wrapper:disabled"?: PtValue | undefined; "wrapper:active"?: PtValue | undefined; "panels:disabled"?: PtValue | undefined; "panels:active"?: PtValue | undefined; "item:none"?: PtValue | undefined; } & { item?: PtValue | PtItemFn<BbTabsItem<K>, BbTabsPtState, "none" | "pill" | "line", { readonly item: BbTabsItem<K>; }> | undefined; } & { "indicator:pill"?: PtValue | undefined; "indicator:line"?: PtValue | undefined; "item:pill"?: PtValue | undefined; "item:line"?: PtValue | undefined; "list:pill"?: PtValue | undefined; "list:line"?: PtValue | undefined; "item:none"?: PtValue | undefined; "list:none"?: PtValue | undefined; "indicator:none"?: PtValue | undefined; "wrapper:none"?: PtValue | undefined; "wrapper:pill"?: PtValue | undefined; "wrapper:line"?: PtValue | undefined; })

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

pt:<part> Type: PtValue | ((ctx: PtItemCtx<BbTabsItem<K>, BbTabsPtState, "none" | "pill" | "line">) => PtItemResult)

Passthrough, attribute form: one attribute per part, pt:<part>, and per part and state, pt:<part>:<state>. Parts: indicator, item, list, panel, panels, root, wrapper. States: active, disabled, line, none, pill.

See example
queryKey Type: stringDefault"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 kebab-case slug (?tab=in-review for inReview), not the raw value, so anything else reading the parameter (a server, an analytics filter) must compare against slotKey(value).

See example
replace Type: booleanDefaultfalse

Replaces the history entry instead of pushing one when the tab changes. Recommended with navigation: the query keeps the tab while Back leaves the page. Leave it off when a tab is a destination of its own (a server visit).

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
server Type: booleanDefaultfalse

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

variant Type: keyof TabsVariantRegistryDefault"pill"

Look of the tab strip: pill (segmented, a sliding raised pill), line (a baseline with a sliding underline) or none (no chrome). Each is a preset of the strip's tokens; register more with the vite plugin's tabsVariants and style .bb-tabs-list--<variant>.

none keeps the behaviour and the structure with no paint. Build a look of your own on it by passing the strip's tokens in pt.wrapper.style, which land inline and need no stylesheet.

See example

v-model

Values kept in sync through v-model.

NameDescription
modelValue Type: K | nullSee example

Events

NameDescription
update:modelValue Type: (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 value 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.

NameDescription
<value> Type: BbTabsSlotProps<K>

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

The panel. Its name is the value in kebab-case (billingInfo and billing_info are #billing-info); with items generated, write #[slotKey(item.value)] rather than restating the rule. Only the active panel is mounted unless it is eager.

See example
<value>:label Type: BbTabsSlotProps<K>

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

The value in kebab-case: inReview and In Review are both #in-review:label. Presentation only, like label.

See example
label Type: BbTabsSlotProps<K>

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

The fallback for every trigger; a <value>:label 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
wrapper:append Type: BbTabsScrollSlotProps

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

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

See example
wrapper:prepend Type: BbTabsScrollSlotProps

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

Renamed and removed

Names from v2 and from earlier 3.0 betas that no longer work, and what to write instead.

  • animateXdisableAnimateXfails silently
  • animateYdisableAnimateYfails silently
  • slotheader:appendwrapper:appendfails silently
  • slotheader:prependwrapper:prependfails silently
  • item fielditems[].keyitems[].valuefails silently

    A tab's identity: what v-model holds and its #<value> / #<value>:label slots; check --fix renames it.

  • slotlabel:<x><x>:labelfails silently
  • querykeyqueryKeyfails silently