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 tabbar

Or install the full npm package:

npm install bigbullui
import { 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.tsx

Zero 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

PropTypeDescription
items{ id: string
labelstring
icon?string
badge?number }[]
value?string
defaultValue?string
onValueChange?(value: string) => void
className?string