📱React Native and flexible tab menus

How to build flexible tab navigation in no time

Mikael Ainalem
4 min readApr 9, 2024
Scrabble pieces to illustrate a flexible tab menu
Photo by Brett Jordan on Unsplash

The tab menu is one of the most fundamental UI elements that exists in mobile apps. Not only does it sit in the bottom of each and every app in the shape of the nav bar. It also appears in many other views where users have the need to jump between different contexts.

Its anatomy is very simple. It is build by the following two elementary parts: 1. two or more labels that users can click on and 2. a highlight that shows the user which one of the options that is currently selected. Besides these two basic building blocks these menus often come with icons and other kinds of decorations. Different colors, drop shadows, etc.

One of my favorite ways to show the highlight is underlining the currently selected option. The underline is so simple but yet intuitive, nothing else is needed. The icing on the cake is to make the underline animate between the different options. I.e. to move and adjust itself to the length of the label when the users selects another option. This tutorial goes through the steps to create a flexible TabMenu that can take a prop of labels as an argument.

1 — Set the stage

What we want to create is a component that takes an array of strings as an argument. This component should have a row (flexDirection: ‘row’) of options. We also want to make these options clickable (TouchableOpacity) and add a label (Text) to each one. Like this:

Clickable options in a tab menu

And here is what the code looks like:

type Props = {
tabs: string[];
};

const TabMenu = ({tabs}: Props) => {
const renderTabs = () => {
return tabs.map((label: string, index: number) => (
<TouchableOpacity key={index} activeOpacity={0.7} onPress={() => {}}>
<View style={[styles.tab]}>
<Text style={styles.tabText}>{label}</Text>
</View>
</TouchableOpacity>
));
};

return <View style={styles.tabContainer}>{renderTabs()}</View>;
};

const styles = StyleSheet.create({
tabContainer: {
flexDirection: 'row',
position: 'relative',
},
tab: {
paddingVertical: 15,
paddingHorizontal: 20,
},
tabText: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
},
});

2 — Measure the tabs

To make the component flexible and to cater for the dynamic underline animation, we want to measure the widths of all the tabs. By knowing the widths, we can calculate the position and interpolate the width of the underline when animating it. First thing first, let’s add the callback to the outer component to measure the width of each label

  // ...
<TouchableOpacity
key={index}
activeOpacity={0.7}
// layout handler to measure each tab
onLayout={event => onTabLayout(event, index)}
onPress={() => {}}>
// ...
</TouchableOpacity>
// ..

And then the function to do the actual measurements. The measuring of widths is quite straight forward. For these we immutably update the two states called tabWidths and tabPositions. For the positions we need to calculate them as the cumulative sum of the widths. Notice how we place the tabPositions and tabWidths in states and not in refs. This is intentional as we want to make sure the component to re-render after each layout cycle is completed. Below is what the code looks like:

  const [tabWidths, setTabWidths] = useState(tabs.map(() => 0));
const [tabPositions, setTabPositions] = useState(tabs.map(() => 0));

// ...
const onTabLayout = (event: LayoutChangeEvent, index: number) => {
const {width} = event.nativeEvent.layout;
const updatedTabWidths = tabWidths.map((tabWidth, anIndex) =>
anIndex === index ? width : tabWidth,
);
setTabWidths(updatedTabWidths);
// Calculate the position of each tab based on the width of the tabs
// [width1, width1 + width2, width1 + width2 + width3, ...]
const updatedTabPositions = updatedTabWidths.map((_, anIndex: number) =>
updatedTabWidths.slice(0, anIndex + 1).reduce((acc, curr) => acc + curr),
);
updatedTabPositions.unshift(0);
updatedTabPositions.pop();
setTabPositions(updatedTabPositions);
};
// ...

3 — Let’s animate!

Animate the underline

Last but not least is the animation. To create an animation in React Native, we use an Animated.Value that we then trigger using the function call Animated.timing. Here is what the part that animates the underline looks like in code:

const TabMenu = ({tabs}: Props) => {
const underlineAnimation = React.useMemo(() => new Animated.Value(0), []);
// ...

const animateUnderline = (index: number) => {
Animated.timing(underlineAnimation, {
toValue: index,
duration: 300,
useNativeDriver: false,
}).start();
};

const handleTabPress = (index: number) => {
animateUnderline(index);
};

const underlinePosition = underlineAnimation.interpolate({
inputRange: tabPositions.map((_, index) => index),
outputRange: tabPositions.map(position => position),
});
const underlineWidth = underlineAnimation.interpolate({
inputRange: tabWidths.map((_, index) => index),
outputRange: tabWidths.map(width => width),
});

const renderTabs = () => {
return tabs.map((label: string, index: number) => (
<TouchableOpacity
// ...
onPress={() => handleTabPress(index)}>
{/* ... */}
</TouchableOpacity>
));
};

return (
<View style={styles.tabContainer}>
{renderTabs()}
<Animated.View
style={[
styles.underline,
{left: underlinePosition, width: underlineWidth},
]}
/>
</View>
);

4 — A look under the hood

Below is a version that shows the Animated.Value and the interpolated tabWidths and tabPositions.

Illustrate the animation and interpolations

Thanks for making it this far and good luck with your tab menus. Please support me with claps, shares, reading my other articles, etc. Cheers!

You can find the code here: https://github.com/ainalem/FlexibleTabs

--

--

Mikael Ainalem

Enthusiastic about software & design, father of 3, freelancer and currently CTO at Norban | twitter: https://twitter.com/mikaelainalem