DEV Community

Cover image for React interactive Components | Tabs
Shubham Tiwari
Shubham Tiwari

Posted on • Updated on

React interactive Components | Tabs

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 }
Enter fullscreen mode Exit fullscreen mode
  • 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);
    };
  }, []);
Enter fullscreen mode Exit fullscreen mode
  • 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>
  )
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
tolive profile image
Sergey Grabak

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...

Collapse
 
shubhamtiwari909 profile image
Shubham Tiwari

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

Collapse
 
catanddog999 profile image
CatAndDog999

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.

Collapse
 
tolive profile image
Sergey Grabak

Using bad practices when they are not very bad in particular case is also a bad practice :)

Thread Thread
 
shubhamtiwari909 profile image
Shubham Tiwari

Agree, will add a note to use unique value for the key in the blog

Thread Thread
 
catanddog999 profile image
CatAndDog999

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...

Collapse
 
mmvergara profile image
Mark Matthew Vergara

This is handy, thanks

Collapse
 
mitchiemt11 profile image
Mitchell Mutandah • Edited

I'm a big fan of Tailwind. I will probably implement the component in one of my projects. Thanks Mysterio!