AniUI

Tabs

Tab navigation with filled and line variants, sizes, vertical layout, icons, disabled state, and RTL support.

Noise-cancelling over-ear headphones with 30-hour battery life.

Web preview — components render natively on iOS & Android
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";

export function MyScreen() {
  return (
    <Tabs defaultValue="overview" size="sm">
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="reviews">Reviews</TabsTrigger>
        <TabsTrigger value="specs">Specs</TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text>Noise-cancelling over-ear headphones with 30-hour battery life.</Text>
      </TabsContent>
      <TabsContent value="reviews">
        <Text>4.0 average from 128 reviews</Text>
      </TabsContent>
      <TabsContent value="specs">
        <Text>Weight: 250g · Battery: 30 hours · Bluetooth: 5.3</Text>
      </TabsContent>
    </Tabs>
  );
}

Installation#

npx @aniui/cli add tabs

Usage#

app/index.tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";

export function MyScreen() {
  return (
    <Tabs defaultValue="overview" size="sm">
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="reviews">Reviews</TabsTrigger>
        <TabsTrigger value="specs">Specs</TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text>Noise-cancelling over-ear headphones with 30-hour battery life.</Text>
      </TabsContent>
      <TabsContent value="reviews">
        <Text>4.0 average from 128 reviews</Text>
      </TabsContent>
      <TabsContent value="specs">
        <Text>Weight: 250g · Battery: 30 hours · Bluetooth: 5.3</Text>
      </TabsContent>
    </Tabs>
  );
}

Line Variant#

Use variant="line" for underline-style tabs instead of the filled background.

Just shipped v0.3 of AniUI 🚀

Web preview — components render natively on iOS & Android
<Tabs defaultValue="posts" variant="line">
  <TabsList>
    <TabsTrigger value="posts">Posts</TabsTrigger>
    <TabsTrigger value="about">About</TabsTrigger>
    <TabsTrigger value="photos">Photos</TabsTrigger>
  </TabsList>
  <TabsContent value="posts">...</TabsContent>
</Tabs>

Vertical#

Use orientation="vertical" for sidebar-style navigation. Works with both filled and line variants.

General account settings.

Web preview — components render natively on iOS & Android
<Tabs defaultValue="general" orientation="vertical" variant="line">
  <TabsList>
    <TabsTrigger value="general">General</TabsTrigger>
    <TabsTrigger value="security">Security</TabsTrigger>
    <TabsTrigger value="notifications">Notifications</TabsTrigger>
  </TabsList>
  <TabsContent value="general">...</TabsContent>
</Tabs>

Disabled#

Add disabled to individual triggers to prevent interaction. Disabled tabs show at reduced opacity.

This tab is active.

Web preview — components render natively on iOS & Android
<Tabs defaultValue="active">
  <TabsList>
    <TabsTrigger value="active">Active</TabsTrigger>
    <TabsTrigger value="disabled" disabled>Disabled</TabsTrigger>
    <TabsTrigger value="other">Other</TabsTrigger>
  </TabsList>
</Tabs>

Icons#

Pass an icon prop to render an icon before the tab label.

Edit your profile information.

Web preview — components render natively on iOS & Android
import Svg, { Path, Circle } from "react-native-svg";

const UserIcon = () => (
  <Svg width={14} height={14} viewBox="0 0 24 24" fill="none"
    stroke="#71717a" strokeWidth={2}>
    <Path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
    <Circle cx={12} cy={7} r={4} />
  </Svg>
);

<Tabs defaultValue="profile">
  <TabsList>
    <TabsTrigger value="profile" icon={<UserIcon />}>Profile</TabsTrigger>
    <TabsTrigger value="settings" icon={<SettingsIcon />}>Settings</TabsTrigger>
  </TabsList>
</Tabs>

Sizes#

Set size on Tabs to control trigger height and text size. Useful for compact layouts on small screens.

Web preview — components render natively on iOS & Android
<Tabs defaultValue="a" size="sm">
  <TabsList>
    <TabsTrigger value="a">Small</TabsTrigger>
    <TabsTrigger value="b">Tabs</TabsTrigger>
  </TabsList>
</Tabs>

<Tabs defaultValue="a" size="md">...</Tabs>

<Tabs defaultValue="a" size="lg">...</Tabs>

RTL#

Tabs support right-to-left layouts automatically. Wrap in DirectionProvider for RTL languages.

محتوى الصفحة الرئيسية

Web preview — components render natively on iOS & Android
import { DirectionProvider } from "@/components/ui/direction-provider";

<DirectionProvider defaultDirection="rtl">
  <Tabs defaultValue="home" variant="line">
    <TabsList>
      <TabsTrigger value="home">الرئيسية</TabsTrigger>
      <TabsTrigger value="settings">الإعدادات</TabsTrigger>
    </TabsList>
    <TabsContent value="home">...</TabsContent>
  </Tabs>
</DirectionProvider>

Components#

ComponentDescription
Tabs

Root container. Controls variant, size, orientation, and state.

TabsList

Container for tab triggers. Adapts styling from variant and orientation.

TabsTrigger

Pressable tab button with active state, disabled, and icon support.

TabsContent

Content panel shown when its tab is active.

Props#

Tabs#

PropTypeDefault
defaultValue
string
required
variant
"filled" | "line"
"filled"
size
"sm" | "md" | "lg"
"md"
orientation
"horizontal" | "vertical"
"horizontal"
className
string
-

TabsTrigger#

PropTypeDefault
value
string
required
disabled
boolean
false
icon
React.ReactNode
-
textClassName
string
-
className
string
-

TabsContent#

PropTypeDefault
value
string
required
className
string
-

All sub-components also accept their respective React Native base props.

Accessibility#

  • accessibilityRole="tab" on triggers, accessibilityRole="tablist" on list, accessibilityRole="tabpanel" on content.
  • accessibilityState tracks selected and disabled.
  • Uses will-change-variable to prevent NativeWind v5 state reset warnings.
  • RTL support via logical properties (border-e, ms-4).

Source#

components/ui/tabs.tsx
import React, { createContext, useContext, useState } from "react";
import { View, Pressable, Text } from "react-native";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";

type TabsVariant = "filled" | "line";
type TabsSize = "sm" | "md" | "lg";
type TabsOrientation = "horizontal" | "vertical";

const TabsCtx = createContext<{
  value: string;
  onValueChange: (v: string) => void;
  variant: TabsVariant;
  size: TabsSize;
  orientation: TabsOrientation;
}>({ value: "", onValueChange: () => {}, variant: "filled", size: "md", orientation: "horizontal" });

export interface TabsProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  defaultValue: string;
  variant?: TabsVariant;
  size?: TabsSize;
  orientation?: TabsOrientation;
  children?: React.ReactNode;
}

export function Tabs({
  defaultValue, variant = "filled", size = "md", orientation = "horizontal",
  className, children, ...props
}: TabsProps) {
  const [value, setValue] = useState(defaultValue);
  return (
    <TabsCtx.Provider value={{ value, onValueChange: setValue, variant, size, orientation }}>
      <View className={cn(orientation === "vertical" && "flex-row", className)} {...props}>
        {children}
      </View>
    </TabsCtx.Provider>
  );
}

const listVariants = cva("", {
  variants: {
    variant: { filled: "rounded-lg bg-muted p-1", line: "border-b border-border" },
    orientation: { horizontal: "flex-row", vertical: "flex-col border-b-0 border-e border-border rounded-none" },
  },
  defaultVariants: { variant: "filled", orientation: "horizontal" },
});

export interface TabsListProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
}

export function TabsList({ className, ...props }: TabsListProps) {
  const { variant, orientation } = useContext(TabsCtx);
  return <View className={cn(listVariants({ variant, orientation }), className)} accessibilityRole="tablist" {...props} />;
}

const triggerSize = cva("will-change-variable items-center justify-center", {
  variants: {
    size: { sm: "min-h-8 py-1 px-2.5", md: "min-h-10 py-2 px-3", lg: "min-h-12 py-2.5 px-4" },
  },
  defaultVariants: { size: "md" },
});

const triggerTextSize = cva("will-change-variable font-medium", {
  variants: {
    size: { sm: "text-xs", md: "text-sm", lg: "text-base" },
  },
  defaultVariants: { size: "md" },
});

export interface TabsTriggerProps extends React.ComponentPropsWithoutRef<typeof Pressable> {
  className?: string;
  textClassName?: string;
  value: string;
  disabled?: boolean;
  icon?: React.ReactNode;
  children: React.ReactNode;
}

export function TabsTrigger({
  value, disabled = false, icon, className, textClassName, children, ...props
}: TabsTriggerProps) {
  const { value: selected, onValueChange, variant, size, orientation } = useContext(TabsCtx);
  const isActive = selected === value;

  return (
    <Pressable
      className={cn(
        triggerSize({ size }),
        orientation === "horizontal" ? "flex-1" : "self-stretch",
        variant === "filled" && "rounded-md",
        variant === "filled" && isActive && "bg-background shadow-sm",
        variant === "line" && isActive && (orientation === "horizontal" ? "border-b-2 border-primary" : "border-e-2 border-primary"),
        disabled && "opacity-40",
        className
      )}
      onPress={() => !disabled && onValueChange(value)}
      disabled={disabled}
      accessible={true}
      accessibilityRole="tab"
      accessibilityState={{ selected: isActive, disabled }}
      {...props}
    >
      <View className="flex-row items-center gap-1.5">
        {icon}
        {typeof children === "string" ? (
          <Text className={cn(triggerTextSize({ size }), isActive ? "text-foreground" : "text-muted-foreground", textClassName)}>
            {children}
          </Text>
        ) : children}
      </View>
    </Pressable>
  );
}

export interface TabsContentProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  value: string;
}

export function TabsContent({ value, className, ...props }: TabsContentProps) {
  const { value: selected, orientation } = useContext(TabsCtx);
  if (selected !== value) return null;
  return <View className={cn(orientation === "horizontal" ? "mt-2" : "ms-4 flex-1", className)} role="tabpanel" {...props} />;
}