Skip to content
WANCRAFT

D.108Navigation & Menus

Mobile Tab Bar

A bottom navigation bar with a sliding indicator, icon bounce, badge counts and an optional raised primary action.

NewReactMotionTailwindintermediate

Overview

Mobile Tab Bar is the bottom navigation of a mobile app. Three to five destinations sit in an evenly divided bar; a shared-layout indicator — a soft pill behind the icon, a solid blob or a top line — slides to the active tab on a spring, and the icon gives a small bounce when selected. Tabs can carry badge counts, labels can be always visible, active-only or hidden, an optional raised primary action sits in the middle, and the bar pads itself for the home indicator with env(safe-area-inset-bottom). Semantically it is one thing: a navigation landmark whose tabs are links (with href) or buttons, and the active one is marked with aria-current="page" — whether you route between pages or swap views in state.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/mobile-tab-bar.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/mobile-tab-bar.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/mobile-tab-bar.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { MobileTabBar, type MobileTabItem } from "@/components/ui/mobile-tab-bar";
import { Bell, Compass, Home, Plus, User } from "lucide-react";

export function MobileTabBarExample() {
  const tabs: MobileTabItem[] = [
    { id: "home", label: "Home", icon: <Home />, href: "/" },
    { id: "explore", label: "Explore", icon: <Compass />, href: "/explore" },
    { id: "inbox", label: "Inbox", icon: <Bell />, href: "/inbox", badge: 3 },
    { id: "profile", label: "Profile", icon: <User />, href: "/profile" },
  ];

  return (
    <MobileTabBar
      items={tabs}
      defaultValue="home"
      primaryAction={{ label: "New post", icon: <Plus /> }}
      fixed
    />
  );
}

03Props

PropTypeDefaultDescription
itemsMobileTabItem[]—Required. 3–5 tabs: { id, label, icon, activeIcon?, badge?, href? }. With href a tab renders a link, otherwise a button.
valuestring—Active tab id (controlled).
defaultValuestring—Initially active tab id (uncontrolled). Defaults to the first item.
onValueChange(id: string) => void—Called when a different tab is pressed.
indicator"pill" | "blob" | "line""pill"Shared-layout indicator style.
labels"always" | "active" | "never""always"Visible labels; hidden labels still name the tab for screen readers.
primaryAction{ label: string; icon: ReactNode; onClick? }—Raised centre button (an action, not a destination).
bouncebooleantrueBounce the icon when its tab becomes active.
spring{ stiffness; damping; mass }{ 500, 36, 0.8 }Spring for the sliding indicator.
accentstring"#FF7448"Indicator, primary action and focus ring colour.
safeAreabooleantrueBottom padding of env(safe-area-inset-bottom) (needs viewport-fit=cover).
fixedbooleanfalsePin the bar to the bottom of the viewport.
labelstring"Main"Accessible name of the nav landmark.
...propsHTMLAttributes<HTMLElement>—Forwarded to the <nav>.

04Features

  • 01Sliding indicator via shared layout (layoutId): soft pill, solid blob or top line
  • 02Icon bounce on selection and spring-popped badge counts (99+)
  • 03Optional raised centre primary action, ringed to sit above the bar
  • 04Labels always, active-only or hidden
  • 05Controlled or uncontrolled; links (href) for routing or buttons for state
  • 06Safe-area padding for home-indicator devices; optional fixed positioning
motion · layoutmotion · springinput · clickinput · pressinput · keyboard

05Accessibility

  • One clear model: a <nav> landmark with a list of links or buttons; the active tab has aria-current="page". It is intentionally not a tablist, because a bottom bar switches app destinations rather than panels in place.
  • Every tab is named by its label, even when labels are visually hidden; badge counts are announced (“Inbox, 3 new”).
  • The primary action is a separate labelled button, not a destination.
  • Icons and the indicator are aria-hidden; visible focus rings on every control.
  • With prefers-reduced-motion the indicator moves instantly and icons do not bounce.

06Limitations

  • Designed for 3–5 tabs; more will crowd the bar on small phones.