Componentnavigation1.05 KB gzipZero dependencies
TabBar
Fixed bottom navigation bar with sliding active indicator pill and badge support
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add tabbarOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { TabBar } from "@/components/ui/tabbar"
<TabBar items={items} value={value} onValueChange={handleChange} />Usage guidance
Use TabBar when a ticket stub surface needs Fixed bottom navigation bar with sliding active indicator pill and badge support Keep props minimal and prefer composition with sibling stubs.
Avoid TabBar for long form flows or dense data grids where a dedicated layout block fits better.
Source code
tabbar.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface TabBarProps {
items: { id: string; label: string; icon?: string; badge?: number }[];
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
className?: string;
}
export function TabBar({
items,
value,
defaultValue,
onValueChange,
className,
...props
}: TabBarProps) {
const [internal, setInternal] = React.useState(defaultValue ?? items[0]?.id ?? "");
const current = value ?? internal;
const handleChange = React.useCallback(
(id: string) => {
setInternal(id);
onValueChange?.(id);
},
[onValueChange]
);
return (
<div
className={cn(
"fixed bottom-0 left-0 right-0 z-50 bg-background border-t border-border/50",
"p-2 sm:p-4 space-x-2",
className
)}
{...props}
>
{items.map((item) => {
const isActive = current === item.id;
const badge = item.badge ?? undefined;
return (
<button
key={item.id}
className={cn(
"flex-1 flex flex-col items-center gap-1 rounded-sm px-2 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isActive
? "text-foreground selected:bg-secondary"
: "text-muted-foreground hover:text-foreground",
isActive && "shadow-sm"
)}
onClick={() => handleChange(item.id)}
aria-pressed={isActive}
role="tab"
>
{item.icon && (
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="currentColor"
focusable="false"
>
<path d={item.icon} />
</svg>
)}
<span className="text-[10px] uppercase tracking-[0.15em]">
{item.label}
</span>
{badge !== undefined && (
<span
className={cn(
"absolute -top-0.5 right-2.5 rounded-full bg-accent text-xs text-accent-foreground w-5 h-5 flex items-center justify-center",
"animate-[stamp_0.4s_ease-out_both]"
)}
>
{badge}
</span>
)}
</button>
);
})}
{/* Active indicator pill */}
<div
className={cn(
"absolute -bottom-1 left-1/2 -translate-x-1/2 h-1 w-6 rounded-full bg-accent/60 transition-transform duration-300 ease-out motion-reduce:transition-none"
)}
/>
</div>
);
}Props
| Prop | Type | Description |
|---|---|---|
| items | { id: string | |
| label | string | |
| icon? | string | |
| badge? | number }[] | |
| value? | string | |
| defaultValue? | string | |
| onValueChange? | (value: string) => void | |
| className? | string |