Algobook
- The developer's handbook
mode-switch
back-button
Buy Me A Coffee
Tue Mar 28 2023

A simple tab filter component in React

I was doing some work the other day on this website, and I did some category filtering with some tab/buttons that allow you readers to easier find articles that you might find interesting. When I was done, I though I will share the code of how I solved my problem. And maybe, someone out there are looking for something similar and might find this helpful.

Problem statement

So the problem is quite straight forward. Imagine we have a list of todos, with different categories, and we want to, in an easy and ux friendly way, filter the todos. In order to solve it, we will create some tabs that are doing this for us.

Result

Our final result should look something like this:

Desktop

Desktop filtered

Mobile

Desktop unfiltered

Prerequisites

Our <Tabs> component

// if using css, import should look like this: import "./Tabs.css"; import style from "./Tabs.module.scss"; interface ITab { label: string; id: string; isActive: boolean; } interface ITabsProps { tabs: ITab[]; onTabClick: (id: string) => void; } export const Tabs = (props: ITabsProps) => { const { tabs, onTabClick } = props; const renderTab = (tab: ITab) => ( <div // If using css, this will not work. Instead do like: className={`tab ${tab.isActive ? "activeTab" : null}`} className={`${style.tab} ${tab.isActive ? style.activeTab : null}`} onClick={() => onTabClick(tab.id)} > <span>{tab.label}</span> </div> ); return <div className={style.tabs}>{tabs.map((tab) => renderTab(tab))}</div>; };

As you can see, our component is quite small, and very dumb, which means it doesn't keep track of any states or logic. It gets provided with everything it should know about.

Some notes

  • isActive is passed in in all tabs to change the design of each tab
  • onTabClick(id) is our callback function which are passed in and should take care of the filter logic, that will be done from our parent component.

Sass/CSS

Let's give our tabs some nice design as well, this is our final stylesheet.

.tabs { display: flex; flex-direction: row; flex-wrap: wrap; gap: 2rem; .tab { display: flex; justify-content: center; align-items: center; flex: 1; font-size: 16px; font-weight: 650; padding: 0.5rem 2rem; border: 1px solid #0069c4; color: #0069c4; min-width: 180px; max-width: 180px; min-height: 40px; max-height: 40px; cursor: pointer; @media only screen and (max-width: 600px) { min-width: 80px; max-width: 80px; min-height: 20px; max-height: 20px; font-size: 12px; font-weight: 500; padding: 0.75rem 0.5rem; } } @media only screen and (max-width: 600px) { gap: 0.5rem; } .activeTab { color: white; background-color: #0069c4; } }

I'm no css expert. Could it be done in a more sleek way? Probably. Does it work? Yeah. Let's proceed!

Some notes

  • As seen, I am using @media tags, that is because we want it to work on mobile as well.
  • I am setting both max/min width/height. I need them to stay the same size and not grow/shrink with the screen.

Test data

I will create some test data here that we will filter out from the parent component

{ "todos": [ { "category": "Cleaning", "workToBeDone": "We need to clean the kids rooms", "id": "1" }, { "category": "Cooking", "workToBeDone": "Food prep!!", "id": "2" }, { "category": "Cleaning", "workToBeDone": "We gotta clean the windows...", "id": "3" }, { "category": "Cleaning", "workToBeDone": "Remove dirt from the floor", "id": "4" }, { "category": "Cooking", "workToBeDone": "Start the oven at 18:00", "id": "5" }, { "category": "Shopping", "workToBeDone": "We need food.", "id": "6" } ] }

Here is some example data with three categories we will filter on.

Use the filter tabs

Let's create our tabs with the above data, and filter some todos!

import { useState } from "react"; import { Tabs } from "components/Tabs"; import { uniqBy } from "lodash"; import json from "./todos.json"; export const Parent = () => { const [activeTabs, setActiveTabs] = useState<string[]>([]); const todos = json.todos; const constructTabs = () => { const isTabActive = (id: string) => activeTabs.includes(id); return uniqBy(todos, "category").map((todo) => ({ id: todo.category, label: todo.category, isActive: isTabActive(todo.category), })); }; const getTodos = () => { if (!activeTabs.length) { return todos; } return todos.filter((todo) => activeTabs.includes(todo.category)); }; const onTabClick = (id: string) => { let tabs = activeTabs; const idx = activeTabs.findIndex((tab) => tab === id); if (idx > -1) { tabs = tabs.filter((t) => t !== id); setActiveTabs(tabs); } else { setActiveTabs([...activeTabs, id]); } }; return ( <div> <Tabs onTabClick={onTabClick} tabs={constructTabs()} /> <div> {getTodos().map((t) => ( <div style={{ border: "1px solid grey", marginTop: "24px", padding: "0 1rem 1rem 1rem", borderRadius: "4px", }} > <h4>{t.category}</h4> {t.workToBeDone} </div> ))} </div> </div> ); };

There we have it folks.

Notes

  • We are creating a function for filtering out our todos based on the selected tabs, getTodos() function.
  • We are creating a function that we will pass in for the actual filtering logic, onTabClick(). This updates the Parent state with the current selected tabs
  • Our constructTabs() function will create the tabs with correct data. NOTE I am using a lodash function here for removing duplicates, since we have multiple objects with the same category. Lodash is an amazing library with great utilities functions. If you haven't used it, start today!

Outro

I hope you find todays guide somewhat useful. This types of components are used in many types of projects and have a great way of improving usability of pages with large amount of content that the user otherwise needs to browse through.

  • Is there something in the code you would have improved?
  • Something you feel my component are missing?
  • Anything else?

Hit me up with any feedback!

signatureTue Mar 28 2023
See all our articles