TabBar
Bottom tab navigation with icons, badges, fixed positioning, and safe-area support.
Usage
Disable fixed when the tab bar is rendered inside a preview frame, modal, or custom shell instead of the viewport.
Best Practices
- Keep item count small enough for thumb navigation, usually three to five primary destinations.
- Use stable primitive
valuefields that map cleanly to routes or view keys. - Set
fixed=falsein embedded previews, drawers, and custom app shells to avoid pinning the bar to the real viewport. - Keep
badgeshort. Use numbers or compact status text; long copy will crowd the icon area. - Do not nest interactive controls inside labels.
TxTabBaralready renders each item as a button.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | '' | Active tab value used by v-model. |
items | TabBarItem[] | [] | 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. |
fixed | boolean | true | Fixes the bar to the viewport bottom with position: fixed. |
safeAreaBottom | boolean | true | Renders an env(safe-area-inset-bottom) spacer below the tab row. |
disabled | boolean | false | Disables all tab buttons and suppresses value updates. |
zIndex | number | 2000 | CSS z-index written to --tx-tab-bar-z-index. |
TabBarItem
| Field | Type | Description |
|---|---|---|
value | string | number | Value emitted when the tab is selected. |
label | string | Visible tab label. |
iconClass | string | Optional icon class rendered above the label. |
badge | string | number | Optional badge shown on the icon area. null, undefined, and empty string are hidden. |
disabled | boolean | Disables only this tab item. |
Slots
No slots. Render tabs through items.
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | TabBarValue | Emitted with the picked item value. |
change | TabBarValue | Emitted 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)
Overview
- The root is a
navlandmark; it is site navigation, not a tab widget, so there is norole="tablist". Each item is abutton, and the active destination carriesaria-current="page"derived frommodelValue. - The indicator measures through the shared
useIndicatorBox, the same readingTxSidebarNavuses: fractional rects rather thanoffsetLeft, the container's border removed because an absolutely positioned indicator resolves against the padding box, and an ancestor transform normalised out. AResizeObserverre-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 thatTxTabs,TxFlatRadioandTxSidebarNavalso ride (Radio's button group keeps the jelly). A new selection glides: the end facing the new item leads on theGLIDEspring (stiffness 420, damping 38) and the trailing end follows the same spring played slower (lag 0.45), so the 84pxmdpill runs to about 98px on the way and gathers on the item. The first measurement, a resize, a font swap and a change ofsizeorindicatorland it in place. - Nothing scales: the indicator's box is only moved and resized, so
pillandblockkeep 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 (thelineon the first or last tab) is still reached exactly. prefers-reduced-motion: reducelands every change in place; the fade stays.- The
pillinset 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.blockshares that box and differs only in paint;dotis centred on it. - Every variant travels on the same x, so changing
indicatornever moves the indicator, only changes what it looks like. sizeships 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 unrecognisedsizefalls back tomd.- Selecting a disabled item, or selecting any item while
disabled=true, emits nothing. - Selecting the already-active item still emits
update:modelValueandchange; debounce duplicate handling in the caller if needed. safeAreaBottomonly controls the spacer node. It can be used with both fixed and non-fixed layouts.zIndexis written as a CSS custom property so app shells can override stacking without deep selectors.
Technologies
- Model contract:
TxTabBaremitsupdate:modelValueandchangeonly when an enabled item is picked while the bar itself is enabled. Disabled items render native disabled buttons. - Layout note:
fixed=truepins the bar to the viewport andsafeAreaBottom=trueadds anenv(safe-area-inset-bottom)spacer. Turn both off inside previews, modals, and embedded shells. - Indicator note: the bar deliberately shares
useIndicatorBoxwithTxSidebarNavand does not grow its own measurement, and moves through the sameuseJellyIndicatorglide 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,heightandopacityitself every frame, and the template binds none of them, so a trip does not re-render the bar. The glide is integrated withspringSteps(packages/tuffex/packages/components/src/liquid/src/spring.ts, 1/240 s substeps), passed in asintegrate, and the scale it writes is always1.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-durationand--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 totypes.tsafterwards — a cold dev server with every cache cleared still emitted the previous prop list, sosizearrived as a fallthrough attribute and read asundefined. Build output was correct throughout, which is what makes it easy to miss.TabBarPropsis still exported for callers; it is just not the source of the runtime list.TxTabsdeclares its props the same way. - Verified coverage:
tab-bar.test.tscovers navigation semantics (nav landmark with norole), thearia-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 thatnonerenders 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 thepillandlineinside the bar at every frame; that a change of variant or size lands in place; and thatprefers-reduced-motion: reducelands a new selection directly. Against the compiled styles it checks that the indicator transitions only itsopacity, 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.tsexportsTabBarItem,TabBarProps,TabBarEmits,TabBarValue,TabBarIndicator, andTabBarSize. - Export entry:
packages/tuffex/packages/components/src/tab-bar/index.tsexportsTabBar,TxTabBar, props/emits/item types, andTxTabBarInstance. - Coverage:
packages/tuffex/packages/components/src/tab-bar/__tests__/tab-bar.test.tsverifies rendering semantics, badges/icons, layout toggles, emissions, disabled behavior, and the indicator's travel, landings and containment.