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:
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 is0
, 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:
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.
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:
Absolutely perfect!