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.
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 tabsUsage#
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 🚀
<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.
<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.
<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.
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.
<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.
محتوى الصفحة الرئيسية
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#
TabsRoot container. Controls variant, size, orientation, and state.
TabsListContainer for tab triggers. Adapts styling from variant and orientation.
TabsTriggerPressable tab button with active state, disabled, and icon support.
TabsContentContent panel shown when its tab is active.
Props#
Tabs#
defaultValuestringrequiredvariant"filled" | "line""filled"size"sm" | "md" | "lg""md"orientation"horizontal" | "vertical""horizontal"classNamestring-TabsTrigger#
valuestringrequireddisabledbooleanfalseiconReact.ReactNode-textClassNamestring-classNamestring-TabsContent#
valuestringrequiredclassNamestring-All sub-components also accept their respective React Native base props.
Accessibility#
accessibilityRole="tab"on triggers,accessibilityRole="tablist"on list,accessibilityRole="tabpanel"on content.accessibilityStatetracksselectedanddisabled.- Uses
will-change-variableto prevent NativeWind v5 state reset warnings. - RTL support via logical properties (
border-e,ms-4).
Source#
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} />;
}

