Tabs Primitive
A collection of layered content sections, referred to as tab panels, displayed individually.
Installation
Install the component via your command line.
npx expo install @rn-primitives/tabs
Install @radix-ui/react-tabs
npx expo install @radix-ui/react-tabs
Copy/paste the following code for web to ~/components/primitives/tabs/tabs.web.tsx
import * as Tabs from '@radix-ui/react-tabs';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, View } from 'react-native';import type { ContentProps, ContentRef, ListProps, ListRef, RootProps, RootRef, TriggerProps, TriggerRef,} from './types';
const TabsContext = React.createContext<RootProps | null>(null);const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, orientation, dir, activationMode, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <TabsContext.Provider value={{ value, onValueChange, }} > <Tabs.Root value={value} onValueChange={onValueChange} orientation={orientation} dir={dir} activationMode={activationMode} asChild > <Component ref={ref} {...viewProps} /> </Tabs.Root> </TabsContext.Provider> ); });
Root.displayName = 'RootWebTabs';
function useRootContext() { const context = React.useContext(TabsContext); if (!context) { throw new Error('Tabs compound components cannot be rendered outside the Tabs component'); } return context;}
const List = React.forwardRef<ListRef, ListProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Tabs.List asChild> <Component ref={ref} {...props} /> </Tabs.List> );});
List.displayName = 'ListWebTabs';
const TriggerContext = React.createContext<{ value: string } | null>(null);const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, value: tabValue, ...props }, ref) => { const Component = asChild ? Slot.Pressable : Pressable; return ( <TriggerContext.Provider value={{ value: tabValue }}> <Tabs.Trigger value={tabValue} asChild> <Component ref={ref} {...props} /> </Tabs.Trigger> </TriggerContext.Provider> ); });
Trigger.displayName = 'TriggerWebTabs';
function useTriggerContext() { const context = React.useContext(TriggerContext); if (!context) { throw new Error( 'Tabs.Trigger compound components cannot be rendered outside the Tabs.Trigger component' ); } return context;}
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, value, tabIndex = -1, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Tabs.Content value={value} asChild> <Component ref={ref} {...props} tabIndex={tabIndex} /> </Tabs.Content> ); });
Content.displayName = 'ContentWebTabs';
export { Content, List, Root, Trigger, useRootContext, useTriggerContext };
Copy/paste the following code for native to ~/components/primitives/tabs/tabs.tsx
import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import type { ContentProps, ContentRef, ListProps, ListRef, RootProps, RootRef, TriggerProps, TriggerRef,} from './types';
interface RootContext extends RootProps { nativeID: string;}
const TabsContext = React.createContext<RootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, value, onValueChange, orientation: _orientation, dir: _dir, activationMode: _activationMode, ...viewProps }, ref ) => { const nativeID = React.useId(); const Component = asChild ? Slot.View : View; return ( <TabsContext.Provider value={{ value, onValueChange, nativeID, }} > <Component ref={ref} {...viewProps} /> </TabsContext.Provider> ); });
Root.displayName = 'RootNativeTabs';
function useRootContext() { const context = React.useContext(TabsContext); if (!context) { throw new Error('Tabs compound components cannot be rendered outside the Tabs component'); } return context;}
const List = React.forwardRef<ListRef, ListProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='tablist' {...props} />;});
List.displayName = 'ListNativeTabs';
const TriggerContext = React.createContext<{ value: string } | null>(null);
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled, value: tabValue, ...props }, ref) => { const { onValueChange, value: rootValue, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; onValueChange(tabValue); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <TriggerContext.Provider value={{ value: tabValue }}> <Component ref={ref} nativeID={`${nativeID}-tab-${tabValue}`} aria-disabled={!!disabled} aria-selected={rootValue === tabValue} role='tab' onPress={onPress} accessibilityState={{ selected: rootValue === tabValue, disabled: !!disabled, }} disabled={!!disabled} {...props} /> </TriggerContext.Provider> ); });
Trigger.displayName = 'TriggerNativeTabs';
function useTriggerContext() { const context = React.useContext(TriggerContext); if (!context) { throw new Error( 'Tabs.Trigger compound components cannot be rendered outside the Tabs.Trigger component' ); } return context;}
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, value: tabValue, ...props }, ref) => { const { value: rootValue, nativeID } = useRootContext();
if (!forceMount) { if (rootValue !== tabValue) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} aria-hidden={!(forceMount || rootValue === tabValue)} aria-labelledby={`${nativeID}-tab-${tabValue}`} role='tabpanel' {...props} /> ); });
Content.displayName = 'ContentNativeTabs';
export { Content, List, Root, Trigger, useRootContext, useTriggerContext };
Copy/paste the following code for types to ~/components/primitives/tabs/types.ts
import type { ForceMountable, PressableRef, SlottablePressableProps, SlottableViewProps, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { value: string; onValueChange: (value: string) => void; /** * Platform: WEB ONLY */ orientation?: 'horizontal' | 'vertical'; /** * Platform: WEB ONLY */ dir?: 'ltr' | 'rtl'; /** * Platform: WEB ONLY */ activationMode?: 'automatic' | 'manual';};
type ListProps = SlottableViewProps;type TriggerProps = SlottablePressableProps & { value: string;};type ContentProps = SlottableViewProps & ForceMountable & { value: string; };
type RootRef = ViewRef;type ListRef = ViewRef;type TriggerRef = PressableRef;type ContentRef = ViewRef;
export type { ContentProps, ContentRef, ListProps, ListRef, RootProps, RootRef, TriggerProps, TriggerRef,};
Copy/paste the following code for exporting to ~/components/primitives/tabs/index.ts
export * from './tabs';export * from './types';
Copy/paste the following code for native to ~/components/primitives/tabs/index.tsx
import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import type { ContentProps, ContentRef, ListProps, ListRef, RootProps, RootRef, TriggerProps, TriggerRef,} from './types';
interface RootContext extends RootProps { nativeID: string;}
const TabsContext = React.createContext<RootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, value, onValueChange, orientation: _orientation, dir: _dir, activationMode: _activationMode, ...viewProps }, ref ) => { const nativeID = React.useId(); const Component = asChild ? Slot.View : View; return ( <TabsContext.Provider value={{ value, onValueChange, nativeID, }} > <Component ref={ref} {...viewProps} /> </TabsContext.Provider> ); });
Root.displayName = 'RootNativeTabs';
function useRootContext() { const context = React.useContext(TabsContext); if (!context) { throw new Error('Tabs compound components cannot be rendered outside the Tabs component'); } return context;}
const List = React.forwardRef<ListRef, ListProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='tablist' {...props} />;});
List.displayName = 'ListNativeTabs';
const TriggerContext = React.createContext<{ value: string } | null>(null);
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled, value: tabValue, ...props }, ref) => { const { onValueChange, value: rootValue, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; onValueChange(tabValue); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <TriggerContext.Provider value={{ value: tabValue }}> <Component ref={ref} nativeID={`${nativeID}-tab-${tabValue}`} aria-disabled={!!disabled} aria-selected={rootValue === tabValue} role='tab' onPress={onPress} accessibilityState={{ selected: rootValue === tabValue, disabled: !!disabled, }} disabled={!!disabled} {...props} /> </TriggerContext.Provider> ); });
Trigger.displayName = 'TriggerNativeTabs';
function useTriggerContext() { const context = React.useContext(TriggerContext); if (!context) { throw new Error( 'Tabs.Trigger compound components cannot be rendered outside the Tabs.Trigger component' ); } return context;}
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, value: tabValue, ...props }, ref) => { const { value: rootValue, nativeID } = useRootContext();
if (!forceMount) { if (rootValue !== tabValue) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} aria-hidden={!(forceMount || rootValue === tabValue)} aria-labelledby={`${nativeID}-tab-${tabValue}`} role='tabpanel' {...props} /> ); });
Content.displayName = 'ContentNativeTabs';
export { Content, List, Root, Trigger, useRootContext, useTriggerContext };
Copy/paste the following code for types to ~/components/primitives/tabs/types.ts
import type { ForceMountable, PressableRef, SlottablePressableProps, SlottableViewProps, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { value: string; onValueChange: (value: string) => void; /** * Platform: WEB ONLY */ orientation?: 'horizontal' | 'vertical'; /** * Platform: WEB ONLY */ dir?: 'ltr' | 'rtl'; /** * Platform: WEB ONLY */ activationMode?: 'automatic' | 'manual';};
type ListProps = SlottableViewProps;type TriggerProps = SlottablePressableProps & { value: string;};type ContentProps = SlottableViewProps & ForceMountable & { value: string; };
type RootRef = ViewRef;type ListRef = ViewRef;type TriggerRef = PressableRef;type ContentRef = ViewRef;
export type { ContentProps, ContentRef, ListProps, ListRef, RootProps, RootRef, TriggerProps, TriggerRef,};
Usage
import * as React from 'react';import * as TabsPrimitive from '@rn-primitives/tabs';import { Text } from 'react-native';
function Example() { const [value, setValue] = React.useState('account'); return ( <TabsPrimitive.Root value={value} onValueChange={setValue} > <TabsPrimitive.Primitive.List> <TabsPrimitive.Trigger value='account'> <Text>Account</Text> </TabsPrimitive.Trigger> <TabsPrimitive.Trigger value='password'> <Text>Password</Text> </TabsPrimitive.Trigger> </TabsPrimitive.Primitive.List> <TabsPrimitive.Content value='account'> <Text>Account content</Text> </TabsPrimitive.Content> <TabsPrimitive.Content value='password'> <Text>Password content</Text> </TabsPrimitive.Content> </TabsPrimitive.Root> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
value | string | |
onValueChange | (value: string ) => void | |
asChild | boolean | (optional) |
disabled | boolean | (optional) |
orientation | ’horizontal’ | ‘vertical’ | Web Only (optional) |
dir | ’ltr’ | ‘rtl’ | Web Only (optional) |
activationMode | ’automatic’ | ‘manual’ | Web Only (optional) |
List
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Trigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Content
Extends View
props
Prop | Type | Note |
---|---|---|
value | string | |
forceMount | boolean | (optional) |
asChild | boolean | (optional) |
useRootContext
Must be used within a Root
component. It provides the following values from the dropdown menu: value
, and onValueChange
.
useTriggerContext
Must be used within a Trigger
component. It provides the following values from the dropdown menu: value
.