A cross-platform Tab View component for React Native.
- Run the example app to see it in action.
- Checkout the example/ folder for source code.
- Smooth animations and gestures
- Scrollable tabs
- Supports both top and bottom tab bars
- Follows Material Design spec
- Highly customizable
- Fully typed with TypeScript
Open a Terminal in the project root and run:
yarn add react-native-tab-view
Now we need to install react-native-gesture-handler
and react-native-reanimated
.
If you are using Expo, to ensure that you get the compatible versions of the libraries, run:
expo install react-native-gesture-handler react-native-reanimated
If you are not using Expo, run the following:
yarn add react-native-reanimated react-native-gesture-handler
If you are using Expo, you are done. Otherwise, continue to the next steps.
Next, we need to link these libraries. The steps depends on your React Native version:
-
React Native 0.60 and higher
On newer versions of React Native, linking is automatic.
To complete the linking on iOS, make sure you have Cocoapods installed. Then run:
cd ios pod install cd ..
-
React Native 0.59 and lower
If you're on an older React Native version, you need to manually link the dependencies. To do that, run:
react-native link react-native-reanimated react-native link react-native-gesture-handler
IMPORTANT: There are additional steps required for react-native-gesture-handler
on Android after linking (for all React Native versions). Check the this guide to complete the installation.
We're done! Now you can build and run the app on your device/simulator.
import * as React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { TabView, SceneMap } from 'react-native-tab-view';
const FirstRoute = () => (
<View style={[styles.scene, { backgroundColor: '#ff4081' }]} />
);
const SecondRoute = () => (
<View style={[styles.scene, { backgroundColor: '#673ab7' }]} />
);
const initialLayout = { width: Dimensions.get('window').width };
export default function TabViewExample() {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: 'first', title: 'First' },
{ key: 'second', title: 'Second' },
]);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
return (
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={initialLayout}
/>
);
}
const styles = StyleSheet.create({
scene: {
flex: 1,
},
});
The package exports a TabView
component which is the one you'd use to render the tab view, and a TabBar
component which is the default tab bar implementation.
Container component responsible for rendering and managing tabs. Follows material design styles by default.
Basic usage look like this:
<TabView
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={SceneMap({
first: FirstRoute,
second: SecondRoute,
})}
/>
State for the tab view. The state should contain the following properties:
index
: a number representing the index of the active route in theroutes
arrayroutes
: an array containing a list of route objects used for rendering the tabs
Each route object should contain the following properties:
key
: a unique key to identify the route (required)title
: title for the route to display in the tab baricon
: icon for the route to display in the tab baraccessibilityLabel
: accessibility label for the tab buttontestID
: test id for the tab button
Example:
{
index: 1,
routes: [
{ key: 'music', title: 'Music' },
{ key: 'albums', title: 'Albums' },
{ key: 'recents', title: 'Recents' },
{ key: 'purchased', title: 'Purchased' },
]
}
TabView
is a controlled component, which means the index
needs to be updated via the onIndexChange
callback.
Callback which is called on tab change, receives the index of the new tab as argument. The navigation state needs to be updated when it's called, otherwise the change is dropped.
Callback which returns a react element to render as the page for the tab. Receives an object containing the route as the argument:
const renderScene = ({ route, jumpTo }) => {
switch (route.key) {
case 'music':
return <MusicRoute jumpTo={jumpTo} />;
case 'albums':
return <AlbumsRoute jumpTo={jumpTo} />;
}
};
You need to make sure that your individual routes implement a shouldComponentUpdate
to improve the performance. To make it easier to specify the components, you can use the SceneMap
helper.
SceneMap
takes an object with the mapping of route.key
to React components and returns a function to use with renderScene
prop.
import { SceneMap } from 'react-native-tab-view';
...
const renderScene = SceneMap({
music: MusicRoute,
albums: AlbumsRoute,
});
Specifying the components this way is easier and takes care of implementing a shouldComponentUpdate
method.
Each scene receives the following props:
route
: the current route rendered by the componentjumpTo
: method to jump to other tabs, takes aroute.key
as it's argumentposition
: animated node which represents the current position
The jumpTo
method can be used to navigate to other tabs programmatically:
this.props.jumpTo('albums');
All the scenes rendered with SceneMap
are optimized using React.PureComponent
and don't re-render when parent's props or states change. If you need more control over how your scenes update (e.g. - triggering a re-render even if the navigationState
didn't change), use renderScene
directly instead of using SceneMap
.
IMPORTANT: Do not pass inline functions to SceneMap
, for example, don't do the following:
SceneMap({
first: () => <FirstRoute foo={this.props.foo} />,
second: SecondRoute,
});
Always define your components elsewhere in the top level of the file. If you pass inline functions, it'll re-create the component every render, which will cause the entire route to unmount and remount every change. It's very bad for performance and will also cause any local state to be lost.
If you need to pass additional props, use a custom renderScene
function:
const renderScene = ({ route }) => {
switch (route.key) {
case 'first':
return <FirstRoute foo={this.props.foo} />;
case 'second':
return <SecondRoute />;
default:
return null;
}
};
Callback which returns a custom React Element to use as the tab bar:
import { TabBar } from 'react-native-tab-view';
...
<TabView
renderTabBar={props => <TabBar {...props} />}
...
/>
If this is not specified, the default tab bar is rendered. You pass this props to customize the default tab bar, provide your own tab bar, or disable the tab bar completely.
<TabView
renderTabBar={() => null}
...
/>
Callback which returns a custom React Element to use as pager.
E.g. you can import ScrollPager
from react-native-tab-view
. It might deliver slightly better experience on iOS.
import { TabView, ScrollPager } from 'react-native-tab-view';
// ...
<TabView
renderPager={props => <ScrollPager { ...props }/>}
// ...
/>
Also, you can use ViewPager
-based pager with React Native Tab View ViewPager Adapter
.
import { TabView } from 'react-native-tab-view';
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter';
// ...
<TabView
renderPager={props => (
<ViewPagerAdapter {...props} transition="curl" showPageIndicator />
)}
// ...
/>
Position of the tab bar in the tab view. Possible values are 'top'
and 'bottom'
. Defaults to 'top'
.
Boolean indicating whether to lazily render the scenes. By default all scenes are rendered to provide a smoother swipe experience. But you might want to defer the rendering of unfocused scenes until the user sees them. To enable lazy rendering, set lazy
to true
.
When you enable lazy
, the unfocused screens will usually take some time to render when they come into focus. You can use the renderLazyPlaceholder
prop to customize what the user sees during this short period.
When lazy
is enabled, you can specify how many adjacent routes should be preloaded with this prop. This value defaults to 0
which means lazy pages are loaded as they come into the viewport.
Callback which returns a custom React Element to render for routes that haven't been rendered yet. Receives an object containing the route as the argument. The lazy
prop also needs to be enabled.
This view is usually only shown for a split second. Keep it lightweight.
By default, this renders null
.
Boolean indicating whether to remove invisible views (such as unfocused screens) from the native view hierarchy to improve memory usage. Defaults to false
.
Note: Don't enable this on iOS where this is buggy and views don't re-appear.
String indicating whether the keyboard gets dismissed in response to a drag gesture. Possible values are:
'auto'
(default): the keyboard is dismissed when the index changes.'on-drag'
: the keyboard is dismissed when a drag begins.'none'
: drags do not dismiss the keyboard.
Boolean indicating whether to enable swipe gestures. Swipe gestures are enabled by default. Passing false
will disable swipe gestures, but the user can still switch tabs by pressing the tab bar.
Determines how relevant is a velocity while calculating next position while swiping. Defaults to 0.2
.
Callback which is called when the swipe gesture starts, i.e. the user touches the screen and moves it.
Callback which is called when the swipe gesture ends, i.e. the user lifts their finger from the screen after the swipe gesture.
Configuration object for the timing animation which occurs when tapping on tabs. Supported properties are:
duration
(number
)
Configuration object for the spring animation which occurs after swiping. Supported properties are:
damping
(number
)mass
(number
)stiffness
(number
)restSpeedThreshold
(number
)restDisplacementThreshold
(number
)
Number for determining how meaningful is gesture velocity for calculating initial velocity of spring animation. Defaults to 0
.
Object containing the initial height and width of the screens. Passing this will improve the initial rendering performance. For most apps, this is a good default:
<TabView
initialLayout={{ width: Dimensions.get('window').width }}
...
/>
Animated value to listen to the position updates. The passed position value will be kept in sync with the current position of the tabs. It's useful for accessing the animated value outside the tab view.
const [position] = useState(() => new Animated.Value(0));
return (
<TabView
position={position}
...
/>
);
Style to apply to the view wrapping each screen. You can pass this to override some default styles such as overflow clipping:
Style to apply to the tab view container.
An object with props to be passed to underlying PanGestureHandler
. For example:
<TabView
gestureHandlerProps={{
maxPointers: 1,
waitFor: [someRef]
}}
...
/>
Material design themed tab bar. To customize the tab bar, you'd need to use the renderTabBar
prop of TabView
to render the TabBar
and pass additional props.
For example, to customize the indicator color and the tab bar background color, you can pass indicatorStyle
and style
props to the TabBar
respectively:
const renderTabBar = props => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: 'white' }}
style={{ backgroundColor: 'pink' }}
/>
);
//...
return (
<TabView
renderTabBar={renderTabBar}
...
/>
);
Function which takes an object with the current route and returns the label text for the tab. Uses route.title
by default.
<TabBar
getLabelText={({ route }) => route.title}
...
/>
Function which takes an object with the current route and returns a boolean to indicate whether to mark a tab as accessible
. Defaults to true
.
Function which takes an object with the current route and returns a accessibility label for the tab button. Uses route.accessibilityLabel
by default if specified, otherwise uses the route title.
<TabBar
getAccessibilityLabel={({ route }) => route.accessibilityLabel}
...
/>
Function which takes an object with the current route and returns a test id for the tab button to locate this tab button in tests. Uses route.testID
by default.
<TabBar
getTestID={({ route }) => route.testID}
...
/>
Function which takes an object with the current route, focused status and color and returns a custom React Element to be used as a icon.
<TabBar
renderIcon={({ route, focused, color }) => (
<Icon
name={focused ? 'abums' : 'albums-outlined'}
color={color}
/>
)}
...
/>
Function which takes an object with the current route, focused status and color and returns a custom React Element to be used as a label.
<TabBar
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>
{route.title}
</Text>
)}
...
/>
Function which takes an object with the current route and returns a custom React Element to be used as a tab indicator.
Function which takes an object with the current route and returns a custom React Element to be used as a badge.
Function to execute on tab press. It receives the scene for the pressed tab, useful for things like scroll to top.
By default, tab press also switches the tab. To prevent this behavior, you can call preventDefault
:
<TabBar
onTabPress={({ route, preventDefault }) => {
if (route.key === 'home') {
preventDefault();
// Do something else
}
}}
...
/>
Function to execute on tab long press, use for things like showing a menu with more options
Custom color for icon and label in the active tab.
Custom color for icon and label in the inactive tab.
Color for material ripple (Android >= 5.0 only).
Opacity for pressed tab (iOS and Android < 5.0 only).
Boolean indicating whether to enable scrollable tabs.
If you set scrollEnabled
to true
, you should also specify a width
in tabStyle
to improve the initial render.
Boolean indicating whether the tab bar bounces when scrolling.
Style to apply to the individual tab items in the tab bar.
By default, all tab items take up the same pre-calculated width based on the width of the container. If you want them to take their original width, you can specify width: 'auto'
in tabStyle
.
Style to apply to the active indicator.
Style to apply to the container view for the indicator.
Style to apply to the tab item label.
Style to apply to the inner container for tabs.
Style to apply to the tab bar container.
Custom pager which can we used inside renderPager
prop. It is based on ScrollView and might bring a slightly better experience on iOS.
It accepts the same set of props as default pager extended with one addition:
When true
, the scroll view bounces when it reaches the end of the content. The default value is false
.
If you want to integrate the tab view with React Navigation's navigation system, e.g. want to be able to navigate to a tab using navigation.navigate
etc, you can use the following official integrations:
- @react-navigation/material-top-tabs for React Navigation 5
- react-navigation-tabs for React Navigation 4
Note that some functionalities are not available with the React Navigation 4 integration because of the limitations in React Navigation. For example, it's possible to dynamically change the rendered tabs.
If you use React Native Navigation by Wix on Android, you need to wrap all your screens that uses react-native-tab-view
with gestureHandlerRootHOC
from react-native-gesture-handler
. Refer react-native-gesture-handler
's docs for more details.
Normally we recommend to use React's local state to manage the navigation state for the tabs. But if you need to use Mobx to manage the navigation state, there is a gotcha you need to be aware of.
Mobx relies on data being accessed in render
to work properly. However, we don't use the index
value inside render
in the library, so Mobx fails to track any changes to the index
. You might see that the tabs don't change on pressing on the tab bar if you have a state like this:
@observable navigationState = {
index: 0,
routes: [
{ key: 'music', title: 'Music' },
{ key: 'albums', title: 'Albums' },
],
};
To workaround this, we need to make sure that index
is accessed in render
. We can refactor our state to something like this for it to work:
@observer
class MyComponent extends React.Component {
@observable index = 0;
@observable routes = [
{ key: 'music', title: 'Music' },
{ key: 'albums', title: 'Albums' },
];
@action handleIndexChange = index => {
this.index = index;
};
render() {
return (
<TabView
navigationState={{ index: this.index, routes: this.routes }}
renderScene={({ route }) => {
/* ... */
}}
onIndexChange={this.handleIndexChange}
/>
);
}
}
The renderScene
function is called every time the index changes. If your renderScene
function is expensive, it's good idea move each route to a separate component if they don't depend on the index, and use shouldComponentUpdate
or React.memo
in your route components to prevent unnecessary re-renders.
For example, instead of:
const renderScene = ({ route }) => {
switch (route.key) {
case 'home':
return (
<View style={styles.page}>
<Avatar />
<NewsFeed />
</View>
);
default:
return null;
}
};
Do the following:
const renderScene = ({ route }) => {
switch (route.key) {
case 'home':
return <HomeComponent />;
default:
return null;
}
};
Where <HomeComponent />
is a PureComponent
if you're using class components:
export default class HomeComponent extends React.PureComponent {
render() {
return (
<View style={styles.page}>
<Avatar />
<NewsFeed />
</View>
);
}
}
Or, wrapped in React.memo
if you're using function components:
function HomeComponent() {
return (
<View style={styles.page}>
<Avatar />
<NewsFeed />
</View>
);
}
export default React.memo(HomeComponent);
We need to measure the width of the container and hence need to wait before rendering some elements on the screen. If you know the initial width upfront, you can pass it in and we won't need to wait for measuring it. Most of the time, it's just the window width.
For example, pass the following initialLayout
to TabView
:
const initialLayout = {
height: 0,
width: Dimensions.get('window').width,
};
The tab view will still react to changes in the dimension and adjust accordingly to accommodate things like orientation change.
If you've a large number of routes, especially images, it can slow the animation down a lot. You can instead render a limited number of routes.
For example, do the following to render only 2 routes on each side:
const renderScene = ({ route }) => {
if (Math.abs(index - routes.indexOf(route)) > 2) {
return <View />;
}
return <MySceneComponent route={route} />;
};
Nesting the TabView
inside a vertical ScrollView
will disable the optimizations in the FlatList
components rendered inside the TabView
. So avoid doing it if possible.
The lazy
option is disabled by default to provide a smoother tab switching experience, but you can enable it and provide a placeholder component for a better lazy loading experience. Enabling lazy
can improve initial load performance by rendering routes only when they come into view. Refer the prop reference for more details.
On Android, enabling removeClippedSubviews
can improve memory usage. This option can also affect rendering performance negatively, so it is disabled by default. So make sure to test it when enabling it. Refer the prop reference for more details.
While developing, you can run the example app to test your changes.
Make sure your code passes TypeScript and ESLint. Run the following to verify:
yarn typescript
yarn lint
To fix formatting errors, run the following:
yarn lint -- --fix
Remember to add tests for your change if possible.