Create a Beautiful Tabbed Carousel in React Native

To understand what I am trying to build, first, let’s see the demo:

As you can see from the gifs, we have two parts to build out this component:

  • The tabs (which you can see at the top)
  • Carousel

We need to make sure the tabs and the carousel items are in sync. This means if we swipe through the items and find items that are different category we also need to activate the tab which matches the item’s category. So let us start building this component.

For the tabs at the top, I am just using horizontal FlatList and for the carousel, I am using the react-native-snap-carousel package. let us then see how did I manage to work this one out.

//DUMMY DATA
const DATA = [
  {
    category: "Home & Living",
    image: require('./assets/f1.jpg')
  },
  {
    category: "Home & Living",
    image: require('./assets/f2.jpg')
  },
  {
    category: "Home & Living",
    image: require('./assets/f4.jpg')
  },
......
];

const TABS = ["Home & Living", "Electronics & Gadgets", "Sports & Outdoors", "Appliances", "Books"]
    
    
const [index, setIndex] = React.useState(0)
const [activeTab, setActiveTab] = React.useState(TABS[0]);
const ref = React.useRef();
const listRef = React.useRef();

const renderTabButtons = () => {
    return (
      <View>
        <FlatList
          ref={listRef}
          horizontal={true}
          data={TABS}
          contentContainerStyle={{ paddingHorizontal: 16, marginBottom: 16 }}
          showsHorizontalScrollIndicator={false}
          keyExtractor={(item, index) => index.toString()}
          renderItem={({ item, index }) => {
            return (
              <TouchableOpacity
                onPress={() => {}}
                style={{ padding: 6, paddingVertical: 20 }}
              >
                <Text>
                  {item}
                </Text>
              </TouchableOpacity>
            );
          }}
        />
      </View>
    );
  };


return (
    <SafeAreaView style={styles.container}>
        <View>{renderTabButtons()}</View>
        <Carousel
            index={index}
            inactiveSlideOpacity={0.4}
            itemWidth={width * 0.85}
            onBeforeSnapToItem={onChangeIndex}
            sliderWidth={width}
            activeAnimationType="spring"
            data={DATA}
            renderItem={renderItem}
            ref={ref}
        />
        <StatusBar style="auto" />
    </SafeAreaView>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    marginTop: 40,
  },
  item: {
    height: height * 0.6,
    borderRadius: 12,
    overflow: 'hidden',
  },
  itemImage: {
    height: '100%',
    width: '100%',
  }
});

So with this init we get the tabs and carousel without any syncing.

If we check the following gif, we will see when we change the index of the carousel and go to a different category product we also make the same category tab active by changing the opacity, and at the same time, you will notice the tabs are getting centered inside our horizontal list. Because if we do not center it the tabs which are outside of the screen width will not come inside our screen and the user will not notice the active tab.

I am pasting the snippet which does this job

/* 
    we change the index when user swipes the carousel
    we also make the tab category active if the items category is changed while swiping 
*/
const onChangeIndex = (index) => {
    setIndex(index);
    if(DATA[index].category !== activeTab)  {
      setActiveTab(DATA[index].category);
    }
}

/* 
    inside flatlist render item ----
    when the active tab category is matched with the item, 
    we change the opacity, so the user knows it is active 
*/
<Text style={ activeTab === item ? { opacity: 1, fontWeight: "bold" } : { opacity: 0.4 }}>
    {item}
</Text>

/* 
    to make sure our tabs are scrolled correctly and withing our screen width,
    we need to add getItemLayout inside our flatlist 
*/
<FlatList 
    ...
    getItemLayout={(_, index) => ({
        length: TABS.length,
        offset: 100 * index,
        index,
    })}
/>

/*
    finally we need to make sure whenever the active tab is changed,
    the list is automatically scrolled to that tab category
*/
React.useEffect(() => {
    if(activeTab) {
        listRef.current.scrollToIndex({
          animated: true,
          viewOffset: 50 * TABS.indexOf(activeTab),
          index: TABS.indexOf(activeTab), // scroll to the correct index
        });
    }
}, [activeTab])

Alright, so far we have seen how to change tabs when we swipe through our items in the carousel. Now let us see how can we achieve to scroll to the correct items when the user presses on tabs.

/*
    This is our tab buttons, when the user press on the tab buttons
    we first scroll to that index, then we set the tab active
*/

<TouchableOpacity
    onPress={() => {
        listRef.current.scrollToIndex({
            animated: true,
            viewOffset: 50 * index,
            index: index,
        });
        setActiveTab(item)
    }}
    style={{ padding: 6, paddingVertical: 20 }}>
    <Text style={activeTab === item ? { opacity: 1, fontWeight: "bold" } : { opacity: 0.4 }}>
        {item}
    </Text>
</TouchableOpacity>

/*
    again we track active tab, so when the active tab changes,
    we scroll the carousel to the first item of that tab category
*/
React.useEffect(() => {
    if(activeTab) {
        ref.current.snapToItem(DATA.map(function(e) { return e.category; }).indexOf(activeTab));
    }
}, [activeTab])

So that’s it! We need to track our active tab and sync the tabs and carousel together in order to achieve this 🙂

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *