A tabbed view webpage

Muhammad Saqib Ilyas
15 min readDec 10, 2023

--

Let’s created a tabbed view web page with HTML, CSS, and JavaScript. Our aim is to create the following:

Preview of what we will create

The content pane has tabs, each displaying different content. The content of only one tab is visible at a time. Clicking on the tab title displays that tab’s content. The active tab title should have a distinctive color and a line underneath it.

Starter HTML

We start with the following HTML:

<!DOCTYPE html>
<html>
<head>
<title>Tabbed page</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;400;500;600;700;800&family=Noto+Sans:wght@100;300;400;500;600;700;800&family=Playpen+Sans:wght@100;300;400;500;600;700;800&family=Poppins:wght@100;300;400;500;600;700;800&family=Roboto:wght@100;300;400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<section class="container">
<div class="tabs-box">
<button class="tabs-button">Home</button>
<button class="tabs-button">Products</button>
<button class="tabs-button">About us</button>
<button class="tabs-button">Contact</button>
</div>
<div class="content-box">
<div class="content">
<h2>Home</h2>
<p>
Welcome to our website. Here, we provide a site for the web. It is meant to be informational. Not for entertainment. If you are looking for entertainment, we refer you to watch Cartoon Network.
</p>
</div>

<div class="content">
<h2>Products</h2>
<p>
Our products cater to our customers' needs. We produce products for all kinds of clientele. We are famous for the following products:
<ul>
<li><a href="#">NoteTaker Free</a> Notes app for personal use.</li>
<li><a href="#">NoteTaker Pro</a> Notes app with the ability to allow editing by up to three other people.</li>
<li><a href="#">NoteTaker Enterprise</a> Notes app with collaborative editing by up to 1000 users.</li>
</ul>
</p>
</div>

<div class="content">
<h2>About us</h2>
<p>
Our team consists of highly skilled acrobats. We know what we are doing and are proud of that. Our products are the result of engineers investing unpaid overtime sacrificing their work-life balance on a consistent basis.
</p>
</div>

<div class="content">
<h2>Contact</h2>
<p>
You may contact us on the following:
<ul>Email: write@example.com</ul>
<ul>Snail mail: 420, Woodhut Dr., London, TX, 12345</ul>
<ul>Phone: 1-800-420-9211</ul>
</p>
</div>
</div>
</section>
<script src="app.js"></script>
</body>
</html>

In the head section, we link to a CSS file. We also link to Google Fonts for a cooler look.

In the body section, we create a section element and give it a class of container. This will house all of the page content. Inside this element, we create two div elements, one with a class of tabs-box and the other with a class of content-box. The former houses are tab buttons for navigation between the tabs, and the latter houses the actual content that is displayed in each tab.

Inside the tabs-box element, we create four button elements, one for each of the tabs. We give each of these elements a class of tabs-button.

Inside the content-box element, we create four div elements, one for each of the tab’s contents. We give each of these div elements a class of content. Each tab’s content starts with an h2 heading, followed by a p element. Some tabs have an unordered list, depending on the content’s needs.

Starter CSS

We start styling the content:

*, ::before, ::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}

html {
font-family: 'Montserrat', 'Poppins', sans-serif;
font-size: 62.5%;
}

body {
background-image: url('background.jpeg');
background-size: cover;
background-position: center;
background-attachment: fixed;
height: 100vh;
}

We start with a CSS reset, setting the margin and padding on all elements to zero, and setting box-sizing to border-box. This ensures that we start with a consistent baseline on all browsers. We also select a default font, and set the font-size property to 62.5%, which means that 1 rem equals 10 pixels.

I downloaded a free background image from unsplash for the page background. I want this image to cover the page background and stay in place. So, I selected this image as the background for the body element. I set the background-size property to cover, so that the image covers the entire page. I set background-position to center, so that the image is centred on the page, and set background-attachement to fixed, so that the background image remains fixed in its place even when the user scrolls. Finally, we set the height of the body element to 100vh so that the body element occupies the entire page height.

Styling the tabbed display

Now, let’s style the tabbed view:

body {
/* Other styles from earlier */
display: flex;
align-items: center;
}

.container {
margin-left: 2rem;
width: 600px;
min-height: 70%;
background-color: #A0CFD3;
padding: 4rem;
border-radius: 2rem;
box-shadow: 0 0 0 0.5rem rgba(0, 0, 0, 0.3);
}

We want to center the tabbed view vertically, so we use Flexbox on its parent, the body element and set the align-item property to center. We select the container class and set its width to 600 pixels, which should be good enough on a large screen.

My background image has a person in the right half. So, I want to position the tabbed view on the left half of the screen. That’s why we didn’t set justify-content to center on the body element. But with the CSS reset, this means that the container is too close to the left edge of the screen. To fix this, we set margin-left property to 2rem. We gave the container a width of 600 pixels, a min-height of 70%, a background color that goes nicely with the background image’s color scheme, a padding so that the content of the box is set apart from its edges. If we don’t give the container a minimum height, the container size will increase and decrease as we switch tabs, and that wouldn’t look pretty. For rounded edges, we use the border-radius property. Finally, we give the box a box shadow. The first two values are x, and y offsets for the light source creating the shadow. Zeros in the first two places means that the shadow spreads equally on all sides of the box. The next two values of 0.5 rem are the blur radius, and the spread radius creating a shadow that decreases in intensity as we go away from the box. The final value is the color, which is a black with an opacity of 30%.

Here’s what the page looks like at the moment:

Our page after some basic styling

I feel that the image is too bright, and I want to “dim” it a little bit. That requires a bit of changes to the CSS:

*, ::before, ::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}

html {
font-family: 'Montserrat', 'Poppins', sans-serif;
font-size: 62.5%;
}

body {
height: 100vh;
display: flex;
align-items: center;
}

body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('background.jpeg');
background-size: cover;
background-position: center;
background-attachment: fixed;
z-index: -1;
opacity: 0.5;
}

.container {
margin-left: 2rem;
width: 600px;
min-height: 70%;
background-color: #A0CFD3;
padding: 4rem;
border-radius: 2rem;
box-shadow: 0 0 0.5rem 0.5rem rgba(0, 0, 0, 0.3);
}

I create a body::after pseudo-element, and move the background image to it, from the body element. I set its z-index property to -1 on this pseudo-element to ensure that the background image is rendered below this element. I set the width and height to 100% so that the pseudo-element is the same size as the body element. I give it a position of fixed, with top and left properties set to 0, so that it overlaps the body element. With content set to empty and opacity set to 0.5, we get the results we wanted.

Reduced opacity for the background image

Styling the tab buttons

The tab buttons are not well laid out. Let’s spread them out in the container.

.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
}

Setting width to 100%, display to flex, and justify-conten to space-around does the trick.

Let’s put a line under the buttons to set it apart from the content underneath and remove the boring button styling.

.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
border-bottom: 0.1rem solid rgba(0, 0, 0, 0.5)
}

.tabs-button {
font-size: 1.8rem;
font-weight: 600;
background: none;
border: none;
color: rgba(0, 0, 0, 0.5);
padding: 0.5rem;
cursor: pointer;
}

We use the border-bottom property to insert a line underneath the buttons. We select the tabs-button class and change the font size and weight so that the button text is prominent. We set the background and border to none so that the buttons don’t look like, well, buttons. We change the font color to a grayish tint. We set a bit of padding to the buttons so that the button text is set apart from the surroundings. Finally, we set the cursor to pointer so that the user gets a visual clue when they hover over the buttons. Here’s what the page looks like now:

The page after styling the buttons

Styling the content

Let’s style the content now:

.content-box {
padding: 2rem;
}

.content h2 {
font-family: 'Playpen sans', sans-serif;
margin-bottom: 1rem;
}

.content {
font-family: 'Noto sans', sans-serif;
font-size: 1.2rem;
}

.content a,
.content a:visited,
.content a:hover,
.content a:active {
text-decoration: none;
color: #60594D;
}

.content a:hover {
color: #8B635C;
}

.content ul {
list-style-type: none;
}

First, we set a bit of padding to space the text away from the edges of the content-box element. We select distinctive fonts for the text and the heading in the content element. We set the font size for the text at 12 pixels, and put a bit of margin below the h2 elements to space the text away from the heading.

The unordered list, and the anchor tags look boring. We get rid of the underline from under the anchor tags by setting text-decoration to none. We also change the color from the deafult blue color. Note that we have to style the :hover, :active, and :visited pseudo-classes, and that it must be done in a specific order. Note that we set the color property for :hover element twice. The later specification takes effect. Finally, we remove the bullets from the unordered list by setting list-style-type to none. Here’s what the page looks like now:

Content styling

Focused tab

Let’s give some distinctive styling to the selected tab. Let’s start by modifying the HTML to give one of the tab buttons a new class name of active.

<div class="tabs-box">
<button class="tabs-button active">Home</button>
<button class="tabs-button">Products</button>
<button class="tabs-button">About us</button>
<button class="tabs-button">Contact</button>
</div>

Next, let’s define the styles for this class.

.tabs-button.active {
color: #3E2F5B;
}

We change the text color for the active class and give it a position of relative. Let’s also display a solid line below the selected tab button using another DOM element. For that, let’s add a div element to the HTML:

<div class="tabs-box">
<button class="tabs-button active">Home</button>
<button class="tabs-button">Products</button>
<button class="tabs-button">About us</button>
<button class="tabs-button">Contact</button>
<div class="line"></div>
</div>

Next, we want to actually make this line visible. That requires some CSS:

.tabs-box {
/* Other styles from earlier */
position: relative;
}

.line {
position: absolute;
display: block;
content: '';
height: 4px;
width: 10px;
background-color: #3E2F5B;
top: 3rem;
left: 0;
}

We define the line class with a position of absolute. This means that this element will be positioned relative to its closest parent that does not have a position property set to anything but static. In this case, it is the div element with the tabs-box class. We want to specify this element’s width and height, so we set its display property to block. We set its content to none. We set its height to 4 pixels for the line thickness. We set the width to 10 pixels, and left to zero. This would show the line To position the line element, we set top to 3rem, and left to 0. This displays the line below the “Home” button. I got the 3rem through a bit of hit and trial until the line coincided with the thin line that we already have under all the buttons. Finally, in order to actually see something, we set the background-color to a purplish color which matches the text color for the active class. But the line doesn’t cover the entire button’s width. To do that, we can modify the width property to 100%.

Next, let’s write some JavaScript button click event handlers so that clicking on the buttons switches the active tab.

const tabs = document.querySelectorAll('.tabs-button')
const line = document.querySelector('.line')
const active = document.querySelector('.active')

tabs.forEach( (tab) => {
tab.addEventListener( 'click', (event) => {
tabs.forEach( (tab) => tab.classList.remove('active'))
event.target.classList.add('active')
line.style.left = event.target.offsetLeft + 'px'
line.style.width = event.target.offsetWidth + 'px'
})
})

We obtain objects corresponding to the elements with the tabs-button, line, and active classes so that we can manipulate their properties. We iterate over the buttons using the forEach() method. To every button, we add a click event listener using the addEventListener() method. We assign an anonymous function as the event handler, which takes the event as an argument. When a button is clicked, all other buttons should relinquish the active class, if any. For that, we iterate over the buttons, again, and remove that class from the classList property. Then, we add that class to the button that was just clicked. We also need to move the underline to the right place, i.e., under the button that was just clicked. For that, we modify the left property to the offsetLeft property of the event target. Since each button can have a different width, we also modify the width property accordingly.

Now, we are able to click on the buttons and get the impression of switching tabs. However, there’s a bit of a glitch. The underline is initially displayed a bit further to the left than its position once we click the “Home” button. In other words, the underline seems to jump to the right if you load the page, then click on “Home”. Let’s fix that.

Inspect the “Home” button element in the browser developer console. Then, click the “Home” button. You’ll notice that the left and width properties appear in the HTML tag.

<div class="line" style="left: 26px; width: 60px;"></div>

We take the above values, and modify the CSS:

.line {
position: absolute;
display: block;
content: '';
height: 4px;
width: 60px;
background-color: #3E2F5B;
top: 3rem;
left: 26px;
}

With that problem fixed, let’s add a bit of a CSS transition to animate the line.

.line {
/* Other styles from earlier */
transition: all 0.5s ease;
}

We added a transition that takes 0.5 seconds. So, as we click on a button, the left property changes. The browser changes this property slowly from its initial to final value over a period of half a second, resulting in an animation.

One tab at a time

Finally, We don’t want to see all pieces of text at the same time. Let’s hide all of the content except the one corresponding to the active tab. For that, we start by setting display to none for the content class. We also define another class named visible that has display set to block.

.content {
font-family: 'Noto sans', sans-serif;
font-size: 1.5rem;
display: none;
}

.content.visible {
display: block;
}

Since initially, in HTML, no content element has the visible class, all of the text disappears. Initially, the “Home” tab content should be visible. Let’s fix that in HTML by adding the visible class to the “Home” tab content.

<div class="content-box">
<div class="content visible">
<h2>Home</h2>
<p>
Welcome to our website. Here, we provide a site for the web. It is meant to be informational. Not for entertainment. If you are looking for entertainment, we refer you to watch Cartoon Network.
</p>
</div>

<div class="content">
<h2>Products</h2>
<p>
Our products cater to our customers' needs. We produce products for all kinds of clientele. We are famous for the following products:
<ul>
<li><a href="#">NoteTaker Free</a> Notes app for personal use.</li>
<li><a href="#">NoteTaker Pro</a> Notes app with the ability to allow editing by up to three other people.</li>
<li><a href="#">NoteTaker Enterprise</a> Notes app with collaborative editing by up to 1000 users.</li>
</ul>
</p>
</div>

<div class="content">
<h2>About us</h2>
<p>
Our team consists of highly skilled acrobats. We know what we are doing and are proud of that. Our products are the result of engineers investing unpaid overtime sacrificing their work-life balance on a consistent basis.
</p>
</div>

<div class="content">
<h2>Contact</h2>
<p>
You may contact us on the following:
<ul>Email: write@example.com</ul>
<ul>Snail mail: 420, Woodhut Dr., London, TX, 12345</ul>
<ul>Phone: 1-800-420-9211</ul>
</p>
</div>
</div>

Next, we want to change the visible content on button clicks. Back to the JavaScript!

const content = document.querySelectorAll('.content')

tabs.forEach( (tab, index) => {
tab.addEventListener( 'click', (event) => {
/* Other code from earlier */
content.forEach( (para) => para.classList.remove('visible'))
content[index].classList.add('visible')
})
})

We obtain an object corresponding to the div elements with the class of content. This will be a collection with four entries, one for each tab. In our click event handler, we iterate over all of these entries, removing the visible class from each. Then, we need to access the content element corresponding to the button clicked. This would be at the same index as the corresponding button has in the buttons array. So, we modify our forEach() over the tabs array, and add the index as argument. We then index into the content array and add a class of visible to it. That takes care of that.

Content animation

Let’s make the content appear as if its sliding from below:

.content {
/* Other styles from earlier */
animation: slide 0.5s ease;
}

@keyframes slide {
from {
transform: translateY(80px);
opacity: 0.3;
}
to {
transform: translateY(0);
opacity: 1;
}
}

We define the animation property and refer to an animation named slide to be played over half a second gradually. The animation defines two keyframes. At first, we set the content to start displaced 80 pixels below its actual position, with an opacity of 30%. Its final position is to its expected position on we web page (thanks to translateY(0)), with an opacity of 100%.

That’s all folks!

With that our project is complete. Note that our styles work for screen sizes of upto 768 pixels width. Any narrower and the display gets distorted. So, we need to have an alternate style for smaller screens. This can be done using media queries. For smaller screens, perhaps we would do away with the tabs and put the different sections one after the other in vertical fashion.

You may download the code from this repository. If you want to copy paste, here’s the HTML:

<!DOCTYPE html>
<html>
<head>
<title>Tabbed page</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;400;500;600;700;800&family=Noto+Sans&family=Playpen+Sans:wght@400;600&family=Poppins:wght@100;300;400;500;600;700;800&family=Roboto:wght@300&display=swap" rel="stylesheet">
</head>
<body>
<section class="container">
<div class="tabs-box">
<button class="tabs-button active">Home</button>
<button class="tabs-button">Products</button>
<button class="tabs-button">About us</button>
<button class="tabs-button">Contact</button>
<div class="line"></div>
</div>
<div class="content-box">
<div class="content visible">
<h2>Home</h2>
<p>
Welcome to our website. Here, we provide a site for the web. It is meant to be informational. Not for entertainment. If you are looking for entertainment, we refer you to watch Cartoon Network.
</p>
</div>

<div class="content">
<h2>Products</h2>
<p>
Our products cater to our customers' needs. We produce products for all kinds of clientele. We are famous for the following products:
<ul>
<li><a href="#">NoteTaker Free</a> Notes app for personal use.</li>
<li><a href="#">NoteTaker Pro</a> Notes app with the ability to allow editing by up to three other people.</li>
<li><a href="#">NoteTaker Enterprise</a> Notes app with collaborative editing by up to 1000 users.</li>
</ul>
</p>
</div>

<div class="content">
<h2>About us</h2>
<p>
Our team consists of highly skilled acrobats. We know what we are doing and are proud of that. Our products are the result of engineers investing unpaid overtime sacrificing their work-life balance on a consistent basis.
</p>
</div>

<div class="content">
<h2>Contact</h2>
<p>
You may contact us on the following:
<ul>Email: write@example.com</ul>
<ul>Snail mail: 420, Woodhut Dr., London, TX, 12345</ul>
<ul>Phone: 1-800-420-9211</ul>
</p>
</div>
</div>
</section>

<script src="app.js"></script>
</body>
</html>

Here’s the CSS:

*, ::before, ::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}

html {
font-family: 'Montserrat', 'Poppins', sans-serif;
font-size: 62.5%;
}

body {
height: 100vh;
display: flex;
align-items: center;
}

body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('background.jpeg');
background-size: cover;
background-position: center;
background-attachment: fixed;
z-index: -1;
opacity: 0.5;
}

.container {
margin-left: 2rem;
width: 600px;
min-height: 70%;
background-color: #A0CFD3;
padding: 4rem;
border-radius: 2rem;
box-shadow: 0 0 0.5rem 0.5rem rgba(0, 0, 0, 0.3);
}

.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
border-bottom: 0.1rem solid rgba(0, 0, 0, 0.5);
position: relative;
}

.tabs-button {
font-size: 1.8rem;
font-weight: 600;
background: none;
border: none;
color: rgba(0, 0, 0, 0.5);
padding: 0.5rem;
cursor: pointer;
}

.tabs-button.active {
color: #3E2F5B;
}

.line {
display: block;
content: '';
height: 4px;
width: 60px;
background-color: #3E2F5B;
position: absolute;
top: 3rem;
left: 26px;
transition: all 0.5s ease;
}

.content-box {
padding: 2rem;
}

.content h2 {
font-family: 'Playpen sans', sans-serif;
margin-bottom: 1rem;
}

.content {
font-family: 'Noto sans', sans-serif;
font-size: 1.5rem;
display: none;
animation: slide 0.5s ease;
}

@keyframes slide {
from {
transform: translateY(80px);
opacity: 0.3;
}
to {
transform: translateY(0);
opacity: 1;
}
}

.content.visible {
display: block;
}

.content a,
.content a:visited,
.content a:hover,
.content a:active {
text-decoration: none;
color: #81171B;
}

.content a:hover {
color: #8B635C;
}

.content ul {
list-style-type: none;
}

Finally, here’s the JavaScript:

const tabs = document.querySelectorAll('.tabs-button')
const line = document.querySelector('.line')
const content = document.querySelectorAll('.content')
const active = document.querySelector('.active')

tabs.forEach( (tab, index) => {
tab.addEventListener( 'click', (event) => {
tabs.forEach( (tab) => tab.classList.remove('active'))
event.target.classList.add('active')
line.style.left = event.target.offsetLeft + 'px'
line.style.width = event.target.offsetWidth + 'px'
content.forEach( (para) => para.classList.remove('visible'))
content[index].classList.add('visible')
})
})

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation