Author avatar

Gaurav Singhal

How to Compose Components to Create Tabs in React

Gaurav Singhal

  • Mar 16, 2020
  • 9 Min read
  • 615 Views
  • Mar 16, 2020
  • 9 Min read
  • 615 Views
Web Development
Front End Web Development
Client-side Framework
React

Introduction

This guide will focus on how to compose components to create a tab component that will display a tab-based navigation view in a declarative manner—that is, with fewer and simpler props with no callbacks at the higher level. All the logic will be inside the <Tabs /> component, making it easy to use anywhere in your project.

The <App /> Component

Inside the <App /> component, call the <Tabs /> component as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default function App() {
  return (
    <div className="App">
      <h1>Tab Component Demo</h1>
      <h2>Start adding tabs to see some magic happen!</h2>
      <Tabs>
        <div title="Tab 1">
          Ut condimentum, ex sit amet rutrum ultrices, nisi velit condimentum
          mi, et sagittis turpis ligula ut neque. Nunc ut arcu velit. Sed
          suscipit nisl est, ut scelerisque elit pretium et.
        </div>
        <div title="Tab 2">
          Phasellus vel nisi lectus. Curabitur ac efficitur quam. Aliquam
          faucibus, nibh non auctor auctor, lectus leo cursus massa, ac ornare
          dolor ante eu libero.
        </div>
        <div title="Tab 3">
          Aliquam erat volutpat. Sed id pulvinar magna. Aenean scelerisque ex
          vitae faucibus faucibus. Proin rutrum est eget odio faucibus tempor.
          Nulla ut tellus non urna congue finibus.
        </div>
      </Tabs>
    </div>
  );
}
jsx

The <Tabs /> component can have multiple child components that represent each tab. Each tab must have a title prop and should contain the tab content as its child.

A Simple <Tabs /> Component

Let's get the basic structure of the component sorted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Tabs extends Component {
  state = {
    activeTabIndex: 0
  };

  render() {
    const { activeTabIndex } = this.state;
    const activeTab = props.children[activeTabIndex];
    return (
      <div>
        <div className="tabs">
          {
            //
          }
        </div>
        <div className="tab-content">{activeTab.props.children}</div>
      </div>
    );
  }
}
jsx

In the state, keep track of the active tab index so that you can track which of the tabs is currently active.

Inside the tabs section, loop through the children and display each title in a <button />. On click of the tab button, set the index as the current active tab. The tab-content section will display the content of the active tab.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
<div className="tabs">
  {props.children.map((tab, i) => (
    <button
      className="tab-btn"
      onClick={() => {
        setActiveTabIndex(i);
      }}
      key={i}
    >
      {tab.props.title}
    </button>
  ))}
</div>
// ...
jsx

Now, if you check out the result, you'll be able to navigate through the tabs. But it's not looking that great; there's no way the user is going to understand which tab is currently active. So, you'll have to create a tab indicator for that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
<div className="tabs">
    {props.children.map((tab, i) => (
      <button
        className="tab-btn"
        onClick={() => {
          setActiveTabIndex(i);
        }}
        key={i}
      >
        {tab.props.title}
      </button>
    ))}
</div>
<div className="tab-indicator-container">
  <div
    className="tab-indicator"
    style={{
      width: 100 / props.children.length + "%",
      transform: `translateX(${activeTabIndex * 100}%)`
    }}
  />
</div>
<div className="tab-content">{activeTab.props.children}</div>
// ...
jsx

Use the transform property to slide the indicator to the currently active tab. To determine the width of the indicator, consider the tab count, which can be retrieved with props.children.length.

Use this CSS to prettify the component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.tabs {
  display: flex;
  align-items: center;
  justify-content: center;
}

.tab-btn {
  display: block;
  flex: 1;
  padding: 8px 20px;
  background: #eee;
  border: 0;
}

.tab-indicator-container {
  display: flex;
  background: #eee;
  margin-bottom: 10px;
}

.tab-indicator {
  height: 3px;
  background: #007bff;
  transition: transform 0.2s;
}

.tab-content {
  padding: 10px;
}
css

And finally, the <Tabs /> component will look as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Tabs extends Component {
  state = {
    activeTabIndex: 0
  };

  render() {
    const { activeTabIndex } = this.state;
    const activeTab = props.children[activeTabIndex];
    return (
      <div>
        <div className="tabs">
          {props.children.map((tab, i) => (
            <button
              className="tab-btn"
              onClick={() => {
                this.setState({ activeTabIndex: i });
              }}
              key={i}
            >
              {tab.props.title}
            </button>
          ))}
        </div>
        <div className="tab-indicator-container">
          <div
            className="tab-indicator"
            style={{
              width: 100 / props.children.length + "%",
              transform: `translateX(${activeTabIndex * 100}%)`
            }}
          />
        </div>
        <div className="tab-content">{activeTab.props.children}</div>
      </div>
    );
  }
}
jsx

Instead of a class component, you can also have a functional component by using the useState hook. Here's how the functional version would look.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const Tabs = props => {
  const [activeTabIndex, setActiveTabIndex] = useState(0);
  const activeTab = props.children[activeTabIndex];
  return (
    <div>
      <div className="tabs">
        {props.children.map((tab, i) => (
          <button
            className="tab-btn"
            onClick={() => {
              setActiveTabIndex(i);
            }}
            key={i}
          >
            {tab.props.title}
          </button>
        ))}
      </div>
      <div className="tab-indicator-container">
        <div
          className="tab-indicator"
          style={{
            width: 100 / props.children.length + "%",
            transform: `translateX(${activeTabIndex * 100}%)`
          }}
        />
      </div>
      <div className="tab-content">{activeTab.props.children}</div>
    </div>
  );
};
jsx

Here's the final output.

Complete Source Code

Here is the full source code for your reference.

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { useState } from "react";
import "./styles.css";

const Tabs = props => {
  const [activeTabIndex, setActiveTabIndex] = useState(0);
  const activeTab = props.children[activeTabIndex];
  return (
    <div>
      <div className="tabs">
        {props.children.map((tab, i) => (
          <button
            className="tab-btn"
            onClick={() => {
              setActiveTabIndex(i);
            }}
            key={i}
          >
            {tab.props.title}
          </button>
        ))}
      </div>
      <div className="tab-indicator-container">
        <div
          className="tab-indicator"
          style={{
            width: 100 / props.children.length + "%",
            transform: `translateX(${activeTabIndex * 100}%)`
          }}
        />
      </div>
      <div className="tab-content">{activeTab.props.children}</div>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <h1>Tab Component Demo</h1>
      <h2>Start adding tabs to see some magic happen!</h2>
      <Tabs>
        <div title="Tab 1">
          Ut condimentum, ex sit amet rutrum ultrices, nisi velit condimentum
          mi, et sagittis turpis ligula ut neque. Nunc ut arcu velit. Sed
          suscipit nisl est, ut scelerisque elit pretium et.{" "}
        </div>
        <div title="Tab 2">
          Phasellus vel nisi lectus. Curabitur ac efficitur quam. Aliquam
          faucibus, nibh non auctor auctor, lectus leo cursus massa, ac ornare
          dolor ante eu libero.{" "}
        </div>
        <div title="Tab 3">
          Aliquam erat volutpat. Sed id pulvinar magna. Aenean scelerisque ex
          vitae faucibus faucibus. Proin rutrum est eget odio faucibus tempor.
          Nulla ut tellus non urna congue finibus.{" "}
        </div>
      </Tabs>
    </div>
  );
}
jsx

styles.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.App {
  font-family: sans-serif;
  text-align: center;
}

.tabs {
  display: flex;
  align-items: center;
  justify-content: center;
}

.tab-btn {
  display: block;
  flex: 1;
  padding: 8px 20px;
  background: #eee;
  border: 0;
}

.tab-indicator-container {
  display: flex;
  background: #eee;
  margin-bottom: 10px;
}

.tab-indicator {
  height: 3px;
  background: #007bff;
  transition: transform 0.2s;
}

.tab-content {
  padding: 10px;
}
css

Conclusion

Tabs make it easier for users to navigate your application, and they are an important part of the UI structure of any application. Most frameworks implement their own version of tabs in their designs, which follow different patterns.

I hope you liked this guide and are looking forward to building some custom components.

5