Exercise: Tab List

Exercise 7 Easy

Prerequisites for the exercise

  1. React Rendering Lists

Objective

Create a TabList component to represent tabbed navigation.

Description

Tabbed navigation, sometimes also referred to as tabs, is a pretty UI common component in web pages.

In its simplest form, we have a collection of tab labels, each of which is clickable, ultimately replacing the content beneath it — known as a tab panel, or a tab pane — with content representing that tab.

Tabbed navigation allows us to present multiple pieces of content, and most importantly a lot of it, without comprising the simplicity of the overall design and layout.

Now that we know what is tabbed navigation, suppose we have a webpage with three tabs as illustrated below:

An example of tabbed navigation
An example of tabbed navigation

At the top, we have a collection of tab labels, each of which is a <button> and is clickable, ultimately showcasing the corresponding tab panel.

The tab panel is represented by a dummy <main> element with an <h1> heading reading the same as the label of that tab. For instance, the second tab is called 'About' and so is the <h1> in the <main> element corresponding to this tab.

In this exercise, you have to implement this tabbed navigation example using React.

You have to create a TabList component that works with the following props:

  • labels specifies a list containing the text to be shown in subsequent tab labels.
  • panels specifies a list containing the content of the <main> element corresponding to each tab.
  • selected specifies the index of the selected tab. By default, it is 0, which means that the first tab is selected.

In addition to this JavaScript logic, your tabbed navigation should additionally be styled with some basic CSS.

In particular:

  • The tab labels should be inlined and padded.
  • The <main> element should have a minimum height of 300px, along with a white background and some nice padding to it (you're free to choose any padding you like).
  • The body should have a light gray background (or you can go with any gray tint as you like to).
  • When a tab is selected, its corresponding label should be styled differently, clearly distinguishing it as the currently selected tab. For example, you could give it a blue background with a white color.

Apart from these basic stylistic requirements, you can improvise with any other styles as you wish to. Your creativity is at work here!

Here's a test setup for you to work with the TabList component:

function App() {
   return (
      <TabList
         labels={['Courses', 'About', 'Contact']}
         panels={[
            <h1>Courses</h1>,
            <h1>About</h1>,
            <h1>Contact</h1>
         ]}
         selected={1}
      />
   );
}

Since the value of selected is 1, initially the second tab should be displayed.

Your final program should work similar in functionality to the program linked below:

Live Example

Hints

Hint 1

Keep track of the currently selected tab's index using a state value.

Hint 2

To render a list of <button>s representing the tab labels, use the map() method on the labels prop. We learnt how this works in the previous React Rendering Lists chapter.

View Solution

New file

Inside the directory you created for this course on React, create a new folder called Exercise-7-Tab-List and put the .js solution files for this exercise within it.

Solution

Let's set up the boilerplate for our TabList component with the props listed above before we begin programming it concretely.

Here's the code to begin with:

function TabList({
   labels,
   panels,
   selected: propSelected = 0
}) {
   // Code to go here.
}

Notice the selected: propSelected = 0 part here. We're extracting the value of the selected prop and assigning it to a local propSelected variable.

We're doing this because later on we'll be using a state value named identically, and so with this change of name, we make sure that we won't encounter an error while defining our state value (using const).

Great. So now let's think on the implementation of TabList.

We'll use a container element for our entire tabbed navigation UI, and call it .tablist. We'll denote it using the HTML <section> element since a tabbed navigation represents a whole, discrete section in a web app.

Within this container, we'll first have a <div> holding all our tab labels, each one named .tablist_label (following the BEM naming convention with a slight twist of our own), and then have a <main> element representing the content (the panel) of the currently selected tab.

The content of this first child <div> of .tablist will simply be rendered by mapping over the labels prop (remember, it's a list) and creating a <button> for each item, whose class would be .tablist_label.

As for the content of the <main> element, it will be obtained from the panels prop (which is also a list) by accessing the item sitting at index selected from it.

Here's the code we get thus far:

import { useState } from 'react';

function TabList({
   labels,
   panels,
   selected: propSelected = 0
}) {
   const [selected, setSelected] = useState(propSelected);

   return (
      <section className="tablist">
         <div>
            {labels.map((label, i) => (
               <button
                  key={i}
                  className="tablist_label"
               >{label}</button>
            ))}
         </div>
         <main>
            {panels[selected]}
         </main>
      </section>
   );
}

Notice the selected state defined here. At any given point, our TabList component needs to be aware of the index of the currently selected tab, and that's precisely what selected is here for.

It's initialized using the value providing in via the selected prop, which itself is stored in a propSelected variable in order to prevent a name collision between the prop and the state constant.

Furthermore, if we take a look into the rendering of the list of <button>s, as stated in the React Rendering Lists chapter, the key prop assigned to each <button> here is required by React since we are rendering a list of items.

Now, there are mainly two things left to be done in this code. One is to make the buttons interactive and second is to assign an additional class to a <button> that corresponds with the currently selected tab.

Starting with the former, what should be done upon the click of each button? Well, the selected state should be changed to the index of the button clicked.

Very simple.

In the following code, we add an onClick handler to the <button>, leveraging the i parameter from the callback provided to map() to be given to setSelected():

import { useState } from 'react';

function TabList({
   labels,
   panels,
   selected: propSelected = 0
}) {
   const [selected, setSelected] = useState(propSelected);

   return (
      <section className="tablist">
         <div>
            {labels.map((label, i) => (
               <button
                  key={i}
                  className="tablist_label"
onClick={() => setSelected(i)} >{label}</button> ))} </div> <main> {panels[selected]} </main> </section> ); }

Now, let's get to the latter concern — giving an additional class to the button corresponding to the currently selected tab.

Again, this is also really simple. We just ought to check while rendering the list of <button>s that whether the current item's index is the same as the value of the selected state, and if it is, add a second .tablist_label--sel to it.

Let's get this done:

import { useState } from 'react';

function TabList({
   labels,
   panels,
   selected: propSelected = 0
}) {
   const [selected, setSelected] = useState(propSelected);

   return (
      <section className="tablist">
         <div>
            {labels.map((label, i) => (
               <button
                  key={i}
className={'tablist_label' + (selected === i ? ' tablist_label--sel' : '')} onClick={() => setSelected(i)} >{label}</button> ))} </div> <main> {panels[selected]} </main> </section> ); }

And with this, we've successfully implemented every single aspect of our tabbed navigation from a functional perspective.

Now, let's finish up with some good old CSS.

We won't go into the details of the CSS code below; it's basic enough that you'll be able to understand each and every bit of it as you read through it:

body {
   background-color: lightgrey;
   margin: 50px;
}

body, button {
   font-family: sans-serif;
}

.tablist main {
   background-color: white;
   padding: 30px;
   min-height: 300px;
   box-sizing: border-box;
}

.tablist_label {
   display: inline-block;
   border: none;
   padding: 15px 20px;
   cursor: pointer;
}

.tablist_label:hover,
.tablist_label--sel {
   background-color: blueviolet;
   color: white
}

Alright, let's now test our program:

Live Example

Absolutely perfect!