Tabs
Use in pages to manage sub-pages
import React from 'react'import type { TabsContentProps } from 'tamagui'import { Button, H5, Separator, SizableText, Tabs, XStack, YStack, isWeb } from 'tamagui'const demos = ['horizontal', 'vertical'] as constconst demosTitle: Record<(typeof demos)[number], string> = {horizontal: 'Horizontal',vertical: 'Vertical',}export function TabsDemo() {const [demoIndex, setDemoIndex] = React.useState(0)const demo = demos[demoIndex]return (// web only fix for position relative<YStack paddingHorizontal="$4" {...(isWeb && { position: 'unset' as any, })} >{demo === 'horizontal' ? <HorizontalTabs /> : <VerticalTabs />}<XStack alignItems="center" space position="absolute" bottom="$3" left="$4" $xxs={{ display: 'none' }} ><Button size="$2" onPress={() => setDemoIndex((x) => (x + 1) % demos.length)}>{demosTitle[demo]}</Button></XStack></YStack>)}const HorizontalTabs = () => {return (<Tabs defaultValue="tab1" orientation="horizontal" flexDirection="column" width={400} height={150} borderRadius="$4" borderWidth="$0.25" overflow="hidden" borderColor="$borderColor" ><Tabs.List separator={<Separator vertical />} disablePassBorderRadius="bottom" aria-label="Manage your account" ><Tabs.Tab flex={1} value="tab1"><SizableText fontFamily="$body">Profile</SizableText></Tabs.Tab><Tabs.Tab flex={1} value="tab2"><SizableText fontFamily="$body">Connections</SizableText></Tabs.Tab><Tabs.Tab flex={1} value="tab3"><SizableText fontFamily="$body">Notifications</SizableText></Tabs.Tab></Tabs.List><Separator /><TabsContent value="tab1"><H5>Profile</H5></TabsContent><TabsContent value="tab2"><H5>Connections</H5></TabsContent><TabsContent value="tab3"><H5>Notifications</H5></TabsContent></Tabs>)}const VerticalTabs = () => {return (<Tabs defaultValue="tab1" flexDirection="row" orientation="vertical" width={400} borderRadius="$4" borderWidth="$0.25" overflow="hidden" borderColor="$borderColor" ><Tabs.List disablePassBorderRadius="end" aria-label="Manage your account" separator={<Separator />} ><Tabs.Tab value="tab1"><SizableText>Profile</SizableText></Tabs.Tab><Tabs.Tab value="tab2"><SizableText>Connections</SizableText></Tabs.Tab><Tabs.Tab value="tab3"><SizableText>Notifications</SizableText></Tabs.Tab></Tabs.List><Separator vertical /><TabsContent value="tab1"><H5 textAlign="center">Profile</H5></TabsContent><TabsContent value="tab2"><H5 textAlign="center">Connections</H5></TabsContent><TabsContent value="tab3"><H5 textAlign="center">Notifications</H5></TabsContent></Tabs>)}const TabsContent = (props: TabsContentProps) => {return (<Tabs.Content backgroundColor="$background" key="tab3" padding="$2" alignItems="center" justifyContent="center" flex={1} borderColor="$background" borderRadius="$2" borderTopLeftRadius={0} borderTopRightRadius={0} borderWidth="$2" {...props} >{props.children}</Tabs.Content>)}
Features
Accessible, easy to compose, customize and animate
Sizable & works controlled or uncontrolled
Supports automatic and manual activation modes for web
Full keyboard navigation
Note: Tabs have landed on v1.7 and not fully ready for runtime. Send us your feedback and we'll address it. We're marking it Beta a such as there may be hopefully minimal breaking changes as we get feedback on the API.
Usage
import { SizableText, Tabs } from 'tamagui'export default () => (<Tabs defaultValue="tab1" width={400}><Tabs.List space><Tabs.Tab value="tab1"><SizableText>Tab 1</SizableText></Tabs.Tab><Tabs.Tab value="tab2"><SizableText>Tab 2</SizableText></Tabs.Tab></Tabs.List><Tabs.Content value="tab1"><H5>Tab 1</H5></Tabs.Content><Tabs.Content value="tab2"><H5>Tab 2</H5></Tabs.Content></Tabs>)
API Reference
Tabs
Root tabs component. Extends Stack. Passing the size
prop to this component will have effect on the descendants.
Props
value
string
The value for the selected tab, if controlled
defaultValue
string
The value of the tab to select by default, if uncontrolled
onValueChange
(value: string) => void
A function called when a new tab is selected
orientation
"horizontal" | "vertical"
Default:
horizontal
The orientation the tabs are layed out
dir
"ltr" | "rtl"
The direction of navigation between toolbar items
activationMode
"manual" | "automatic"
Default:
automatic
Whether or not a tab is activated automatically or manually. Not applicable on mobile
Tabs.List
Container for the trigger buttons. Supports scrolling by extending Group. You can disable passing border radius to children by passing disablePassBorderRadius
.
Props
loop
boolean
Default:
true
Whether or not to loop over after reaching the end or start of the items. Used mainly for managing keyboard navigation
Tabs.Trigger
Extends Button, adding:
Props
value
string
The value for the tabs state to be changed to after activation of the trigger
onInteraction
(type: InteractionType, layout: TabTriggerLayout | null) => void
Used for making custom indicators when trigger interacted with
unstyled
boolean
When true, remove all default tamagui styling
Tabs.Content
Where each tab's content will be shown. Extends ThemeableStack, adding:
Props
value
string
Will show the content when the value matches the state of Tabs root
forceMount
boolean
Default:
false
Used to force mounting when more control is needed. Useful when controlling animation with Tamagui animations
Examples
Animations
Here is a demo with more advanced animations using AnimatePresence and Trigger's onInteraction
prop.
import React from 'react'import type { StackProps, TabLayout, TabsTabProps } from 'tamagui'import {AnimatePresence,Button,H5,SizableText,Tabs,XStack,YStack,styled,} from 'tamagui'const demos = ['background', 'underline'] as constconst demosTitle: Record<(typeof demos)[number], string> = {background: 'Background Indicator',underline: 'Underline Indicator',}export const TabsAdvancedDemo = () => {const [demoIndex, setDemoIndex] = React.useState(0)const demo = demos[demoIndex]return (<>{demo === 'underline' ? <TabsAdvancedUnderline /> : <TabsAdvancedBackground />}<XStack alignItems="center" gap="$4" position="absolute" bottom="$3" left="$4" $xxs={{ display: 'none' }} ><Button size="$2" onPress={() => setDemoIndex((x) => (x + 1) % demos.length)}>{demosTitle[demo]}</Button></XStack></>)}const TabsAdvancedBackground = () => {const [tabState, setTabState] = React.useState<{ currentTab: string /** * Layout of the Tab user might intend to select (hovering / focusing) */ intentAt: TabLayout | null /** * Layout of the Tab user selected */ activeAt: TabLayout | null /** * Used to get the direction of activation for animating the active indicator */ prevActiveAt: TabLayout | null }>({activeAt: null,currentTab: 'tab1',intentAt: null,prevActiveAt: null,})const setCurrentTab = (currentTab: string) => setTabState({ ...tabState, currentTab })const setIntentIndicator = (intentAt) => setTabState({ ...tabState, intentAt })const setActiveIndicator = (activeAt) =>setTabState({ ...tabState, prevActiveAt: tabState.activeAt, activeAt })const { activeAt, intentAt, prevActiveAt, currentTab } = tabState// 1 = right, 0 = nowhere, -1 = leftconst direction = (() => {if (!activeAt || !prevActiveAt || activeAt.x === prevActiveAt.x) {return 0}return activeAt.x > prevActiveAt.x ? -1 : 1})()const handleOnInteraction: TabsTabProps['onInteraction'] = (type, layout) => {if (type === 'select') {setActiveIndicator(layout)} else {setIntentIndicator(layout)}}return (<Tabs value={currentTab} onValueChange={setCurrentTab} orientation="horizontal" size="$4" padding="$2" height={150} flexDirection="column" activationMode="manual" backgroundColor="$background" borderRadius="$4" position="relative" ><YStack><AnimatePresence>{intentAt && (<TabsRovingIndicator borderRadius="$4" width={intentAt.width} height={intentAt.height} x={intentAt.x} y={intentAt.y} />)}</AnimatePresence><AnimatePresence>{activeAt && (<TabsRovingIndicator borderRadius="$4" theme="active" width={activeAt.width} height={activeAt.height} x={activeAt.x} y={activeAt.y} />)}</AnimatePresence><Tabs.List disablePassBorderRadius loop={false} aria-label="Manage your account" gap="$2" backgroundColor="transparent" ><Tabs.Tab unstyled paddingVertical="$2" paddingHorizontal="$3" value="tab1" onInteraction={handleOnInteraction} ><SizableText>Profile</SizableText></Tabs.Tab><Tabs.Tab unstyled paddingVertical="$2" paddingHorizontal="$3" value="tab2" onInteraction={handleOnInteraction} ><SizableText>Connections</SizableText></Tabs.Tab><Tabs.Tab unstyled paddingVertical="$2" paddingHorizontal="$3" value="tab3" onInteraction={handleOnInteraction} ><SizableText>Notifications</SizableText></Tabs.Tab></Tabs.List></YStack><AnimatePresence exitBeforeEnter custom={{ direction }} initial={false}><AnimatedYStack key={currentTab}><Tabs.Content value={currentTab} forceMount flex={1} justifyContent="center"><H5 textAlign="center">{currentTab}</H5></Tabs.Content></AnimatedYStack></AnimatePresence></Tabs>)}const TabsAdvancedUnderline = () => {const [tabState, setTabState] = React.useState<{ currentTab: string /** * Layout of the Tab user might intend to select (hovering / focusing) */ intentAt: TabLayout | null /** * Layout of the Tab user selected */ activeAt: TabLayout | null /** * Used to get the direction of activation for animating the active indicator */ prevActiveAt: TabLayout | null }>({activeAt: null,currentTab: 'tab1',intentAt: null,prevActiveAt: null,})const setCurrentTab = (currentTab: string) => setTabState({ ...tabState, currentTab })const setIntentIndicator = (intentAt) => setTabState({ ...tabState, intentAt })const setActiveIndicator = (activeAt) =>setTabState({ ...tabState, prevActiveAt: tabState.activeAt, activeAt })const { activeAt, intentAt, prevActiveAt, currentTab } = tabState// 1 = right, 0 = nowhere, -1 = leftconst direction = (() => {if (!activeAt || !prevActiveAt || activeAt.x === prevActiveAt.x) {return 0}return activeAt.x > prevActiveAt.x ? -1 : 1})()const handleOnInteraction: TabsTabProps['onInteraction'] = (type, layout) => {if (type === 'select') {setActiveIndicator(layout)} else {setIntentIndicator(layout)}}return (<Tabs value={currentTab} onValueChange={setCurrentTab} orientation="horizontal" size="$4" height={150} flexDirection="column" activationMode="manual" backgroundColor="$background" borderRadius="$4" ><YStack><AnimatePresence>{intentAt && (<TabsRovingIndicator width={intentAt.width} height="$0.5" x={intentAt.x} bottom={0} />)}</AnimatePresence><AnimatePresence>{activeAt && (<TabsRovingIndicator theme="active" active width={activeAt.width} height="$0.5" x={activeAt.x} bottom={0} />)}</AnimatePresence><Tabs.List disablePassBorderRadius loop={false} aria-label="Manage your account" borderBottomLeftRadius={0} borderBottomRightRadius={0} paddingBottom="$1.5" borderColor="$color3" borderBottomWidth="$0.5" backgroundColor="transparent" ><Tabs.Tab unstyled paddingHorizontal="$3" paddingVertical="$2" value="tab1" onInteraction={handleOnInteraction} ><SizableText>Profile</SizableText></Tabs.Tab><Tabs.Tab unstyled paddingHorizontal="$3" paddingVertical="$2" value="tab2" onInteraction={handleOnInteraction} ><SizableText>Connections</SizableText></Tabs.Tab><Tabs.Tab unstyled paddingHorizontal="$3" paddingVertical="$2" value="tab3" onInteraction={handleOnInteraction} ><SizableText>Notifications</SizableText></Tabs.Tab></Tabs.List></YStack><AnimatePresence exitBeforeEnter custom={{ direction }} initial={false}><AnimatedYStack key={currentTab}><Tabs.Content value={currentTab} forceMount flex={1} justifyContent="center"><H5 textAlign="center">{currentTab}</H5></Tabs.Content></AnimatedYStack></AnimatePresence></Tabs>)}const TabsRovingIndicator = ({ active, ...props }: { active?: boolean } & StackProps) => {return (<YStack position="absolute" backgroundColor="$color5" opacity={0.7} animation="100ms" enterStyle={{ opacity: 0, }} exitStyle={{ opacity: 0, }} {...(active && { backgroundColor: '$color8', opacity: 0.6, })} {...props} />)}const AnimatedYStack = styled(YStack, {flex: 1,x: 0,opacity: 1,animation: '100ms',variants: {// 1 = right, 0 = nowhere, -1 = leftdirection: {':number': (direction) => ({enterStyle: {x: direction > 0 ? -25 : 25,opacity: 0,},exitStyle: {zIndex: 0,x: direction < 0 ? -25 : 25,opacity: 0,},}),},} as const,})