Components/TabBar

TabBar

Bottom tab navigation with icons, badges, fixed positioning, and safe-area support.

VerifiedSince 0.3.4

Usage

Disable fixed when the tab bar is rendered inside a preview frame, modal, or custom shell instead of the viewport.

Loading demo...

Best Practices

  • Keep item count small enough for thumb navigation, usually three to five primary destinations.
  • Use stable primitive value fields that map cleanly to routes or view keys.
  • Set fixed=false in embedded previews, drawers, and custom app shells to avoid pinning the bar to the real viewport.
  • Keep badge short. Use numbers or compact status text; long copy will crowd the icon area.
  • Do not nest interactive controls inside labels. TxTabBar already renders each item as a button.

API Reference

Props

PropTypeDefaultDescription
modelValuestring | number''Active tab value used by v-model.
itemsTabBarItem[][]Tabs rendered from left to right.
indicator'none' | 'pill' | 'line' | 'block' | 'dot''pill'Sliding indicator behind the active item. pill raises a surface, block tints the same box, line runs a rule along the top edge, dot marks the item, none is colour only.
size'sm' | 'md' | 'lg''md'Geometry tier. Drives bar height, icon size, label size and the pill inset, delivered as inline CSS variables.
fixedbooleantrueFixes the bar to the viewport bottom with position: fixed.
safeAreaBottombooleantrueRenders an env(safe-area-inset-bottom) spacer below the tab row.
disabledbooleanfalseDisables all tab buttons and suppresses value updates.
zIndexnumber2000CSS z-index written to --tx-tab-bar-z-index.

TabBarItem

FieldTypeDescription
valuestring | numberValue emitted when the tab is selected.
labelstringVisible tab label.
iconClassstringOptional icon class rendered above the label.
badgestring | numberOptional badge shown on the icon area. null, undefined, and empty string are hidden.
disabledbooleanDisables only this tab item.

Slots

No slots. Render tabs through items.

Events

EventPayloadDescription
update:modelValueTabBarValueEmitted with the picked item value.
changeTabBarValueEmitted with the same value after a non-disabled item is picked.

Indicator

indicator slides one surface behind the active destination instead of relying on colour alone. pill raises a surface the way TxFlatRadio's thumb does; block fills the same box with a tint instead, for a bar on a card where another shadow would only add noise; line runs a rule along the bar's top edge; dot marks the item with a small centred dot; none is the colour-only bar. The names match TxTabs' indicatorVariant so the two read as one family.

Every variant glides like the rest of the tabs family (TxTabs, TxFlatRadio, TxSidebarNav): its two ends ride springs, so it lengthens a little on the way to a new tab and gathers as it lands, and it never squashes or scales. At either end of the bar an end stops at the edge instead of leaving it, so a frame with overflow: hidden never cuts it off.

size is the same three-step ladder TxFlatRadio uses, for the same reason: a bar inside a compact panel and a bar at the bottom of a phone screen are not the same control at the same size.

TabBar (indicator)

Loading demo...

Overview

  • The root is a nav landmark; it is site navigation, not a tab widget, so there is no role="tablist". Each item is a button, and the active destination carries aria-current="page" derived from modelValue.
  • The indicator measures through the shared useIndicatorBox, the same reading TxSidebarNav uses: fractional rects rather than offsetLeft, the container's border removed because an absolutely positioned indicator resolves against the padding box, and an ancestor transform normalised out. A ResizeObserver re-measures, so the indicator does not strand after a resize or a font swap.
  • The indicator moves on the shared indicator engine (useJellyIndicator), on the glide material that TxTabs, TxFlatRadio and TxSidebarNav also ride (Radio's button group keeps the jelly). A new selection glides: the end facing the new item leads on the GLIDE spring (stiffness 420, damping 38) and the trailing end follows the same spring played slower (lag 0.45), so the 84px md pill runs to about 98px on the way and gathers on the item. The first measurement, a resize, a font swap and a change of size or indicator land it in place.
  • Nothing scales: the indicator's box is only moved and resized, so pill and block keep the size tier's insets through a trip and the lengthening runs along the bar, never across it.
  • At the bar's two ends an end of the indicator that would leave the bar stops at its edge: the engine's walls are the bar's padding-box width (clientWidth). A target flush with the edge (the line on the first or last tab) is still reached exactly.
  • prefers-reduced-motion: reduce lands every change in place; the fade stays.
  • The pill inset is arithmetic on the measured box, not a CSS margin: an absolutely positioned box with an explicit width and height ignores margin for sizing, which left the pill at the item's full height and hanging out of the bar. block shares that box and differs only in paint; dot is centred on it.
  • Every variant travels on the same x, so changing indicator never moves the indicator, only changes what it looks like.
  • size ships as inline CSS variables rather than size classes, so a caller can override one value — say --tx-tab-bar-height — without restating a tier. The bar height in particular has to be a variable: the indicator measures the item box, so a class-based height would move the indicator through a path that never reads it. An unrecognised size falls back to md.
  • Selecting a disabled item, or selecting any item while disabled=true, emits nothing.
  • Selecting the already-active item still emits update:modelValue and change; debounce duplicate handling in the caller if needed.
  • safeAreaBottom only controls the spacer node. It can be used with both fixed and non-fixed layouts.
  • zIndex is written as a CSS custom property so app shells can override stacking without deep selectors.

Technologies

  • Model contract: TxTabBar emits update:modelValue and change only when an enabled item is picked while the bar itself is enabled. Disabled items render native disabled buttons.
  • Layout note: fixed=true pins the bar to the viewport and safeAreaBottom=true adds an env(safe-area-inset-bottom) spacer. Turn both off inside previews, modals, and embedded shells.
  • Indicator note: the bar deliberately shares useIndicatorBox with TxSidebarNav and does not grow its own measurement, and moves through the same useJellyIndicator glide as the rest of the tabs family. A bar that travels must not drift from the controls that travel beside it.
  • Motion note: the engine writes the indicator's transform, width, height and opacity itself every frame, and the template binds none of them, so a trip does not re-render the bar. The glide is integrated with springSteps (packages/tuffex/packages/components/src/liquid/src/spring.ts, 1/240 s substeps), passed in as integrate, and the scale it writes is always 1.000. No CSS transition sits on those properties: it would re-ease every written frame and the indicator would trail its own spring. --tx-tab-bar-indicator-duration and --tx-tab-bar-indicator-ease, which drove the old CSS travel, no longer take part.
  • Props are declared as a runtime object, not defineProps<TabBarProps>(). The SFC compiler resolves an imported props interface by reading the sibling module, and it does not pick up fields added to types.ts afterwards — a cold dev server with every cache cleared still emitted the previous prop list, so size arrived as a fallthrough attribute and read as undefined. Build output was correct throughout, which is what makes it easy to miss. TabBarProps is still exported for callers; it is just not the source of the runtime list. TxTabs declares its props the same way.
  • Verified coverage: tab-bar.test.ts covers navigation semantics (nav landmark with no role), the aria-current="page" active item, icons, badges, z-index CSS variable, fixed/safe-area toggles, enabled emissions, disabled bar/item blocking, each size tier's inline CSS variables with an unknown-size fallback, every indicator variant's class once the first measurement has rendered the node, and that none renders no indicator node. With stubbed rects and fake timers it checks that the first measurement lands the pill in place; that a new selection glides rather than jumping — running longer than the pill on the way, never scaling, its height unchanged — and settles exactly on the next item's inset box; that a round trip to both ends keeps the pill and line inside the bar at every frame; that a change of variant or size lands in place; and that prefers-reduced-motion: reduce lands a new selection directly. Against the compiled styles it checks that the indicator transitions only its opacity, reduced motion included.
  • Component source: packages/tuffex/packages/components/src/tab-bar/src/TxTabBar.vue.
  • Types: packages/tuffex/packages/components/src/tab-bar/src/types.ts exports TabBarItem, TabBarProps, TabBarEmits, TabBarValue, TabBarIndicator, and TabBarSize.
  • Export entry: packages/tuffex/packages/components/src/tab-bar/index.ts exports TabBar, TxTabBar, props/emits/item types, and TxTabBarInstance.
  • Coverage: packages/tuffex/packages/components/src/tab-bar/__tests__/tab-bar.test.ts verifies rendering semantics, badges/icons, layout toggles, emissions, disabled behavior, and the indicator's travel, landings and containment.
查看源码
packages/tuffex/packages/components/src/tab-bar/index.ts