Building a pinned left nav menu with React Navigation v3

Sean Holbert
4 min readMay 26, 2019

--

Update from April 4th 2024

The below post is an outdated approach for React Navigation < v4. After v4, React Navigation removed Switch navigators began recommending using a ternary operator to render different components, to represent a navigation flow where you switch content. More importantly, they introduced drawerType 'permanent' to Drawer navigation in React Navigation v5. This is the modern way to implement pinned left navigation going, rendering the below post obsolete.

Original Post

React Navigation provides most common navigation layouts you will need. There is one popular case however where there is no out-of-the-box solution.

Slack’s desktop client uses a pinned left nav for navigation

If you want to navigate via a left set of tabs that stay open as you browse your content, React Navigation doesn’t have a default navigator for this. createDrawerNavigator comes close, but it will push your content to the right when the drawer opens, and adds an overlay disabling interactivity while the drawer is open.

A left nav menu is particularly useful for building tablet apps where more horizontal screen real estate is available. We will build a simple navigator with four tabs that are pinned left (see below).

1. Build the LeftTabs component

We’ll start by building the tabs we need to render on the left. We’ll just have to pass in isSelected, a tabName, and a callback onPress to notify the component’s owner when a new tab has been selected.

2. Use the Switch Navigator to power the content that gets displayed in each tab

Now that we need to hook our tabs up to drive what content will be displayed. This requires structuring our react-navigation hierarchy. We’ll use createSwitchNavigator to represent our content being displayed.

import { Ionicons } from '@expo/vector-icons';// Some simple components to represent our content
const sharedStyles = {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
};
const HomeContent = ({screenProps}) => {
return (
<View style={[sharedStyles, {backgroundColor: '#3BC'}]}>
<NavigationEvents onDidFocus={screenProps.onDidFocus} />
<Ionicons name='ios-planet' size={230} color='pink' />
</View>
);
}
const PlacesContent = ({navigation, screenProps}) => {
return (
<View style={[sharedStyles, {backgroundColor: '#81A'}]}>
<NavigationEvents onDidFocus={screenProps.onDidFocus} />
<Ionicons name='ios-pin' size={230} color='red' />
<TouchableHighlight onPress={() => navigation.navigate('Home')}>
<Text style={{color: 'red', fontSize: 60}}>Go Home</Text>
</TouchableHighlight>
</View>
);
}
const PeopleContent = ({screenProps}) => {
return (
<View style={[sharedStyles, {backgroundColor: '#A53'}]}>
<NavigationEvents onDidFocus={screenProps.onDidFocus} />
<Ionicons name='ios-contacts' size={230} color='blue' />
</View>
);
}
const MeContent = ({screenProps}) => {
return (
<View style={[sharedStyles, {backgroundColor: '#FAA'}]}>
<NavigationEvents onDidFocus={screenProps.onDidFocus} />
<Ionicons name='ios-contact' size={230} color='purple' />
</View>
);
}

Now let’s map these pieces of content to routes in the switch navigator.

import {createSwitchNavigator} from 'react-navigation';const AppContent = createSwitchNavigator(
{
'Home': {
screen: HomeContent,
},
'Places': {
screen: PlacesContent
},
'People': {
screen: PeopleContent
},
'Me': {
screen: MeContent
}
},
{ //Set to true if you want to destroy state on tab changes.
resetOnBlur: false
}
);
export default AppContent;

3. Hook up the LeftTabs and AppContent components

Finally, we will need to wrap the LeftTabs and AppContent components to structure the layout and trigger navigation actions when a user clicks on a tab.

import AppContent from './AppContent'
import LeftTabs from './LeftTabs';
class App extends React.PureComponent {
static router = AppContent.router;
constructor() {
super();
this.state = {
selectedTab: 'Home'
}
}
onTabPressed = (routeName) => {
this.props.navigation.navigate({routeName});
}
handleNavChange = ({action}) => {
// Handles when navigation is triggered from within a tabs content:
if (action.type === NavigationActions.NAVIGATE) {
this.setState({
selectedTab: action.routeName
});
}
}
render() {
const {
navigation
} = this.props;
const tabs = [
{
tabName: 'Home',
tabIcon: 'ios-planet'
},
{
tabName: 'Places',
tabIcon: 'ios-pin'
},
{
tabName: 'People',
tabIcon: 'ios-contacts'
},
{
tabName: 'Me',
tabIcon: 'ios-contact'
}
]
return (
<View style={styles.box}>
<LeftTabs
width={'25%'}
tabs={tabs}
onTabPressed={this.onTabPressed}
selectedTab={this.state.selectedTab}/>
<View style={styles.content}>
<AppContent
navigation={navigation}
screenProps={{onDidFocus: this.handleNavChange}}/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
box: {
flexDirection: 'row',
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
content: {
flex: 1
}
});
export default createAppContainer(App)

And there we have it! The full example is available here:

Leave a comment if you have any questions.

--

--

Sean Holbert
Sean Holbert

Written by Sean Holbert

App Developer, Open Source Contributor. Github @wildseansy

No responses yet