There are so many ways to show content only on demand, to save some space on your website, instead of showing everything all at once. Accordions show answers only when you click on the question, Anchor menus scroll to content when you click on them, tooltips show content, when you hover over a text or link, Dialogs pop up when you click on a button or other element. Tabs do the same with different visuals. It is just a list of menu and when you click on each of them it will display a content that was initially hidden.(just like accordions).

Let’s start with the requirements first:

So what is the main logic in the tab system

The only thing you have to keep in memory is the index of the currently opened tab. So each time you click on a tab you will store the index of the clicked item in a state hook.

Let’s start first with me providing you the code. There will be 3 files, the first one will be the parent of the other two:

This is the parent component: TabSystem.tsx

import {useEffect, useState} from 'react';
import TabListItem from './TabListItem';
import TabContent from './TabContent';
import styled from 'styled-components'; 

const Tabs = styled.div`
    font-family: Segoe UI, Arial, sans-serif;
    border: 1px solid #999;
    border-radius: 10px;
    overflow: hidden;
    ul {
       padding: 0;
       margin: 0;
       display: flex;
       list-style: none;
       border-bottom: 1px solid #999;
       li {
            background: #eee;
            padding: 20px;
            border-right: 1px solid #999;
            cursor: pointer;
            &.activeTab {
                background: #fff;
                box-shadow: 0 5px 0 #A9DDD6 inset; 
    div {
        padding: 20px;
        line-height: 2;
        font-size: 18px;
        text-align: center;
    img {
        width: 50%;
        margin: 0 auto 50px;
        padding: 20px;
        box-shadow:0 0 10px 10px rgba(0,0,0,.2);
        display: block;

const TabSystem = () => {
    const [data, setData] = useState([]);
    const [activeItem, setActiveItem] = useState(0);

    useEffect(() => {
            .then(response => response.json())
            .then(response => setData(response));

    return (
                    data.map(({tabTitle}, index) => (    
                data.map(({tabContent, tabImage}, index) => (  
                        isHidden={activeItem !== index} 
export default TabSystem;

Then the component with the list of tab titles: TabListItem.tsx

interface TabListItemProps {
    title: string;
    index: number;
    activeItem: number;
    setActiveItem: React.Dispatch<React.SetStateAction<number>>;

const TabListItem = ({title, index, activeItem, setActiveItem}: TabListItemProps) => {
    return (
            {...(activeItem === index? {className: "activeTab"} : {})}
            onClick={() => setActiveItem(index)}>

export default TabListItem;

The third one is the one with the tab content: TabContent.tsx

interface tabContentProps {
    tabContent: string;
    tabImage: string;
    isHidden: boolean;

const TabContent = ({tabContent, tabImage, isHidden}: tabContentProps) => {
    return (
        <div hidden={isHidden}>
            <p><img src={tabImage} /></p>

export default TabContent;

I’ve organized the TabSystem component with all 3 files in the same folder, called Tabs:

First things first, we create 2 useState hooks, one to hold the data coming from the fetched Json(by default it’s empty array) and the other(activeItem) for holding the index of the currently opened tab. Then on the load event of the component we fetch the data and set the data as the new value on “data” state.

For the JSON I used mock data with the help of Mockoon and it looks like this:

It is an array of objects that have the following fields: tabTitle, tabImage and tabContent.

We have a list (UL) and the <li> items with the tab titles in the TabListItem component. Also we have separately the TabContent component holding the image and the description text. I created 2 separate mappings because the titles are a separate section and than the tab contents are just below the tab list. Structurally there is no relation between them.

In both cases we used the key prop otherwise we get an error. We are sending only the required information for the child components from the JSON data instead of sending the entire data. We send down to the TabListItem component the activeItem state and also the setter for this activeItem(setActiveItem) because when a tab title is clicked it will send back the clicked item’s index to be stored in the activeItem state and of course to rerender both components with the new active tab title and active tab content.

Let’s talk about the TabListItem component

The types for the component props are defined in the TabListItemProps interface. Now to have the “activeTab” we made a conditional class attribute and the condition is that the current active item’s index has to be equal with the tab titles index. On the onClick event we can set a new active item sending up to the parent component.

How about the TabListContent component?

In the same manner we create an interface for the props of this component. What content will be shown and what will be hidden is dependent on the hidden attributes value and that values was defined in the parent component:

The same evaluation is done here, if the index of this iteration does not match the currently active item stored in the activeItem state then the isHidden prop will get a true value and eventually the hidden attribute will get a true, so it won’t display, only the item with the index from the state will show.

What about the style of this “masterpiece”?

Well, I used styled components for this one. I called the main wrapper “Tabs” and then targeted the descendants of this element.

Here we are at the end of this article, thank you for your attention and of course like and subscribe and comment if you have anything to add.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.