AniUI

Tab Bar

Bottom tab bar with badge support and active states.

Installation#

npx @aniui/cli add tab-bar
Web preview — components render natively on iOS & Android
import { TabBar, TabBarItem } from "@/components/ui/tab-bar";
import { Ionicons } from "@expo/vector-icons";

export function MyScreen() {
  const [active, setActive] = useState("home");

  return (
    <TabBar>
      <TabBarItem
        active={active === "home"}
        icon={<Ionicons name="home" size={20} />}
        label="Home"
        onPress={() => setActive("home")}
      />
      <TabBarItem
        active={active === "search"}
        icon={<Ionicons name="search" size={20} />}
        label="Search"
        onPress={() => setActive("search")}
      />
      <TabBarItem
        active={active === "inbox"}
        icon={<Ionicons name="mail" size={20} />}
        label="Inbox"
        badge={3}
        onPress={() => setActive("inbox")}
      />
      <TabBarItem
        active={active === "profile"}
        icon={<Ionicons name="person" size={20} />}
        label="Profile"
        onPress={() => setActive("profile")}
      />
    </TabBar>
  );
}

Usage#

app/index.tsx
import { TabBar, TabBarItem } from "@/components/ui/tab-bar";
import { Ionicons } from "@expo/vector-icons";

export function MyScreen() {
  const [active, setActive] = useState("home");

  return (
    <TabBar>
      <TabBarItem
        active={active === "home"}
        icon={<Ionicons name="home" size={20} />}
        label="Home"
        onPress={() => setActive("home")}
      />
      <TabBarItem
        active={active === "search"}
        icon={<Ionicons name="search" size={20} />}
        label="Search"
        onPress={() => setActive("search")}
      />
      <TabBarItem
        active={active === "inbox"}
        icon={<Ionicons name="mail" size={20} />}
        label="Inbox"
        badge={3}
        onPress={() => setActive("inbox")}
      />
      <TabBarItem
        active={active === "profile"}
        icon={<Ionicons name="person" size={20} />}
        label="Profile"
        onPress={() => setActive("profile")}
      />
    </TabBar>
  );
}

Props#

TabBar#

PropTypeDefault
variant
"default" | "card" | "transparent"
"default"
className
string
-
children
React.ReactNode
-

TabBarItem#

PropTypeDefault
active
boolean
false
icon
React.ReactNode
-
label
string
-
badge
number
-
onPress
() => void
-
className
string
-

Accessibility#

  • Bottom tab navigation with badge support.
  • Each tab has accessibilityRole="tab" with selected state.

Source#

components/ui/tab-bar.tsx
import React from "react";
import { View, Pressable, Text } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const tabBarVariants = cva("flex-row border-t pb-6 pt-2 px-2", {
  variants: {
    variant: {
      default: "bg-background border-border",
      card: "bg-card border-border",
      transparent: "bg-transparent border-transparent",
    },
  },
  defaultVariants: { variant: "default" },
});

export interface TabBarProps
  extends React.ComponentPropsWithoutRef<typeof View>,
    VariantProps<typeof tabBarVariants> {
  className?: string;
  children?: React.ReactNode;
}

export function TabBar({ variant, className, children, ...props }: TabBarProps) {
  return (
    <View className={cn(tabBarVariants({ variant }), className)} accessibilityRole="tablist" {...props}>
      {children}
    </View>
  );
}

export interface TabBarItemProps extends React.ComponentPropsWithoutRef<typeof Pressable> {
  className?: string;
  active?: boolean;
  icon?: React.ReactNode;
  label?: string;
  badge?: number;
  onPress?: () => void;
}

export function TabBarItem({ active, icon, label, badge, className, onPress, ...props }: TabBarItemProps) {
  return (
    <Pressable
      className={cn("flex-1 items-center justify-center gap-1 min-h-12", className)}
      accessible={true}
      accessibilityRole="tab"
      accessibilityState={{ selected: active }}
      onPress={onPress}
      {...props}
    >
      {icon && <View>{icon}</View>}
      {label && (
        <Text className={cn("text-xs", active ? "text-primary font-medium" : "text-muted-foreground")}>
          {label}
        </Text>
      )}
      {badge !== undefined && badge > 0 && (
        <View className="absolute -top-1 right-1/4 bg-destructive rounded-full min-w-5 h-5 items-center justify-center px-1">
          <Text className="text-destructive-foreground text-[10px] font-bold">
            {badge > 99 ? "99+" : badge}
          </Text>
        </View>
      )}
    </Pressable>
  );
}