Hello Everyone, today we are going to discuss reusable tabs in React. We will create a Tab component with some props and use Tailwind to style the component.
Let's get started...
Import, props and its usage
{/*
data = [
{
header:"header",
content:HTML element, react element or plain text
},
.
.
] // data format should be like this
keyboardNavigation = true/false // Used with right and left arrow key for navigation
tabContainerClasses = tailwind classes // container wrapping the entire tab component
headerContainerClasses = tailwind classes // container having all the tabs button
headerClasses = tailwind classes // individual tab
contentContainerClasses = tailwind classes // container having the content
*/}
import React, { useEffect, useState } from 'react'
const Tabs = ({
data,
keyboardNavigation = true,
tabContainerClasses = "",
headerContainerClasses = "",
headerClasses = "",
contentContainerClasses = ""
}) => { //content }
- We have imported the useEffect and useState hooks which we will use for state management and triggering event handlers.
- We also have some props, their usage is already provided on top of the component using comments.
States and handlers
const [activeTab, setActiveTab] = useState(0);
const handleActiveTab = (index) => setActiveTab(index)
useEffect(() => {
const keyDownHandler = event => {
if (!keyboardNavigation) return
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
event.preventDefault();
setActiveTab(prev => (event.key === "ArrowRight" ? ((prev + 1) % data.length) : (prev === 0 ? data.length - 1 : prev - 1)))
}
};
document.addEventListener('keydown', keyDownHandler);
return () => {
document.removeEventListener('keydown', keyDownHandler);
};
}, []);
- Firstly we have created an active tab state to track the active tab and change the content accordingly
- Then created a click handler method for tabs, it will set the active tab as the index of the tab which is clicked making it the active tab and change the content.
- We have used "useEffect" to check for the keypress event, here we are checking if the keyboardNavigation prop is true, then only implement the keyboard navigation for tha tab
- Then we are checking for right and left arrow key using "event.key", after that we are setting the tab value based on the key we pressed.
- If the right arrow key is pressed, then increment the active tab value by one from the previous value and increment it until it is equal to the data length - 1 value, at that point, set the active tab back to 0.
- If the left arrow key is pressed, then decrement the active tab value by one and if the active tab value is 0 then set the active tab value to the data length - 1 value, which is the last element in the data array.
- Then we have attached the method to the keydown event listener, we are also cleaning up the event listener if the component unmounts using the return statement inside "useEffect" by using the removeEventListener.
UI part -
return (
<div className={`bg-slate-200 p-8 rounded-lg border border-slate-500 ${tabContainerClasses}`}>
<div className={`flex gap-4 flex-wrap mb-6 ${headerContainerClasses}`}>
{
data?.map((tab, index) => {
return (
<button onClick={() => handleActiveTab(index)} key={index} className={`p-2 rounded-lg border border-solid border-blue-400 ${headerClasses} ${index === activeTab ? "!bg-slate-800 !text-blue-200" : ""}`}>
{tab.header}
</button>
)
})
}
</div>
<div className={`p-2 rounded-lg border border-solid border-blue-400 min-h-16 bg-slate-800 text-blue-200 ${contentContainerClasses}`}>
{data[activeTab]?.content}
</div>
</div>
)
- Firstly we have created a main container for the tab component with some default classes and "tabContainerClasses" prop to override the default classes.
- Inside the main component, we have header component where we have mapped the data headers with a button element, which have onClick hanlder to set the active tab to the index of the header, making it the active tab and change the content accordingly. It also has some default classes with headerClasses to override those default ones, with active tab as a condition to change the styles if the the tab is active to dark.
- Then we have the content container with default classes and contentContainerClasses to override default ones, inside it we have the content for the respective header, here we are accessing the content using activeTab as the index, so when the activeTab changes, the index changes and so does the content.
NOTE - USE UNIQUE ID OR VALUE FOR THE KEY ATTRIBUTE INSIDE MAP METHOD
Full code with usage
{/*
data = [
{
header:"header",
content:HTML element, react element or plain text
},
.
.
] // data format should be like this
keyboardNavigation = true/false // Used with right and left arrow key for navigation
tabContainerClasses = tailwind classes // container wrapping the entire tab component
headerContainerClasses = tailwind classes // container having all the tabs button
headerClasses = tailwind classes // individual tab
contentContainerClasses = tailwind classes // container having the content
*/}
import React, { useEffect, useState } from 'react'
const Tabs = ({
data,
keyboardNavigation = true,
tabContainerClasses = "",
headerContainerClasses = "",
headerClasses = "",
contentContainerClasses = ""
}) => {
const [activeTab, setActiveTab] = useState(0);
const handleActiveTab = (index) => setActiveTab(index)
useEffect(() => {
const keyDownHandler = event => {
if (!keyboardNavigation) return
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
event.preventDefault();
setActiveTab(prev => (event.key === "ArrowRight" ? ((prev + 1) % data.length) : (prev === 0 ? data.length - 1 : prev - 1)))
}
};
document.addEventListener('keydown', keyDownHandler);
return () => {
document.removeEventListener('keydown', keyDownHandler);
};
}, []);
return (
<div className={`bg-slate-200 p-8 rounded-lg border border-slate-500 ${tabContainerClasses}`}>
<div className={`flex gap-4 flex-wrap mb-6 ${headerContainerClasses}`}>
{
data?.map((tab, index) => {
return (
<button onClick={() => handleActiveTab(index)} key={index} className={`p-2 rounded-lg border border-solid border-blue-400 ${headerClasses} ${index === activeTab ? "!bg-slate-800 !text-blue-200" : ""}`}>
{tab.header}
</button>
)
})
}qx
</div>
<div className={`p-2 rounded-lg border border-solid border-blue-400 min-h-16 bg-slate-800 text-blue-200 ${contentContainerClasses}`}>
{data[activeTab]?.content}
</div>
</div>
)
}
export default Tabs
// App.js
"use client" // for next js
import Container from "./components/Tabs"
export default function Home() {
const data = [
{ header: "Tab 1", content:<p className="font-mono text-base text-blue-300">Content 1</p>},
{ header: "Tab 2", content:<h1 className="font-mono text-2xl text-blue-300">Content 2</h1>},
{ header: "Tab 3", content:<p className="font-mono text-base text-blue-300">Content 3</p>}
]
return (
<main className='min-h-screen'>
<Container
data={data}
tabContainerClasses="w-fit bg-gradient-to-r from-blue-500 via-violet-500 to-purple-500"
headerContainerClasses="bg-slate-900 p-4 rounded-xl"
headerClasses="bg-blue-100 text-black"
contentContainerClasses="bg-white p-6"/>
</main>
)
}
Feel free to give suggestions in comments to improve the component and make it more reusable and efficient.
THANK YOU FOR CHECKING THIS POST
You can contact me on -
Instagram - https://www.instagram.com/supremacism__shubh/
LinkedIn - https://www.linkedin.com/in/shubham-tiwari-b7544b193/
Email - shubhmtiwri00@gmail.com
^^You can help me with some donation at the link below Thank youππ ^^
β --> https://www.buymeacoffee.com/waaduheck <--
Also check these posts as well
https://dev.to/shubhamtiwari909/website-components-you-should-know-25nm
https://dev.to/shubhamtiwari909/smooth-scrolling-with-js-n56
https://dev.to/shubhamtiwari909/swiperjs-3802
https://dev.to/shubhamtiwari909/custom-tabs-with-sass-and-javascript-4dej
Top comments (8)
I suggest to not use an index as a key when mapping an array
data?.map((tab, index) => {
It should be unique number or string, 'header' field should be ok.
More info about it react.dev/learn/rendering-lists#wh...
Yeah that's just for the demo purpose, as i think people would try some different data format and provide their key name with data, that's why I didn't provide the specific name for the key attribute
From the point of view of the source code, there is actually no problem in using the index as the key in this demo, because this is a static list.
Using bad practices when they are not very bad in particular case is also a bad practice :)
Agree, will add a note to use unique value for the key in the blog
This is not a bad practice. In the static list, using index as the key can reuse nodes faster. Of course, this is only for static lists.
I recommend you to read this article: developerway.com/posts/react-key-a...
This is handy, thanks
I'm a big fan of Tailwind. I will probably implement the component in one of my projects. Thanks Mysterio!