React interactive Components | Tabs

Shubham Tiwari - Sep 2 '23 - - Dev Community

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .