Tabs

A tab component for switching between different content.

Basic Tabs

Loading...

Tabs with custom icons

Loading...

Tabs with custom variants and transition

Loading...

Usage Guide

Install the clsx and tailwind-merge packages:

npm install clsx tw-merge

Next, add cn utility:

import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from "clsx";

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

Copy and paste the following component source.

"use client";

import React from "react";
import { cn } from "@/lib/utils";
import { motion, Variants, Transition, AnimatePresence } from "framer-motion";

type AnimationType = "line" | "none";

interface TabContextType {
    activeTab: string;
    setActiveTab: (tab: string) => void;
    animationType: AnimationType;
    instanceId: string;
}

const TabContext = React.createContext<TabContextType | null>(null);

const useTabContext = () => {
    const context = React.useContext(TabContext);
    if (!context) {
        throw new Error("Tab components must be used within a Tabs component");
    }
    return context;
};

/* -------------------------------------------------------------------------------------------------
 * Tabs component
 * -------------------------------------------------------------------------------------------------
 */
interface TabsProps {
    children: React.ReactNode;
    defaultTab: string;
    className?: string;
    animationType?: AnimationType;
    instanceId?: string;
}

const Tabs = ({
    children,
    defaultTab,
    className,
    animationType = "line",
    instanceId = Math.random().toString(36).slice(2)
}: TabsProps) => {
    const [activeTab, setActiveTab] = React.useState(defaultTab);

    return (
        <TabContext.Provider value={{ activeTab, setActiveTab, animationType, instanceId }}>
            <div className={cn("w-full", className)}>{children}</div>
        </TabContext.Provider>
    );
};

/* -------------------------------------------------------------------------------------------------
 * TabList component
 * -------------------------------------------------------------------------------------------------
 */
interface TabListProps {
    children: React.ReactNode;
    className?: string;
}

const TabList = ({ children, className }: TabListProps) => {
    return (
        <div
            className={cn(
                "relative flex gap-2 border-b border-gray-200 dark:border-zinc-700",
                className
            )}
        >
            {children}
        </div>
    );
};

/* -------------------------------------------------------------------------------------------------
 * Tab component
 * -------------------------------------------------------------------------------------------------
 */
interface TabProps {
    children: React.ReactNode;
    value: string;
    className?: string;
    icon?: React.ReactNode;
}

const Tab = ({ children, value, className, icon }: TabProps) => {
    const { activeTab, setActiveTab, animationType, instanceId } = useTabContext();
    const isActive = activeTab === value;

    const getAnimationProps = () => {
        switch (animationType) {
            case "line":
                return {
                    layoutId: `activeTab-${instanceId}`,
                    className:
                        "absolute -bottom-[1px] left-0 right-0 h-[2px] rounded-full bg-black dark:bg-white"
                };
            default:
                return {};
        }
    };

    return (
        <motion.button
            onClick={() => setActiveTab(value)}
            className={cn(
                "relative px-4 py-2 text-sm font-medium transition-colors flex items-center gap-2",
                {
                    "text-gray-500 hover:text-black dark:text-gray-400 dark:hover:text-white":
                        !isActive
                },
                className
            )}
        >
            {icon && <span>{icon}</span>}
            {children}
            {isActive && animationType !== "none" && <motion.div {...getAnimationProps()} />}
        </motion.button>
    );
};

/* -------------------------------------------------------------------------------------------------
 * TabPanels component
 * -------------------------------------------------------------------------------------------------
 */

interface TabPanelsProps {
    children: React.ReactNode;
    className?: string;
}

const TabPanels = ({ children, className }: TabPanelsProps) => {
    return (
        <div className={cn("mt-3 p-3 rounded border dark:border-zinc-700", className)}>
            {children}
        </div>
    );
};

/* -------------------------------------------------------------------------------------------------
 * TabPanel component
 * -------------------------------------------------------------------------------------------------
 */

interface TabPanelProps {
    children: React.ReactNode;
    value: string;
    className?: string;
    variants?: Variants;
    transition?: Transition;
}

const TabPanel = ({
    children,
    value,
    className,
    variants = undefined,
    transition = undefined
}: TabPanelProps) => {
    const { activeTab } = useTabContext();
    if (activeTab !== value) return null;

    return (
        <AnimatePresence mode="wait">
            {activeTab === value && (
                <motion.div
                    initial="initial"
                    animate="animate"
                    exit="exit"
                    variants={variants}
                    transition={transition}
                    className={cn(className)}
                >
                    {children}
                </motion.div>
            )}
        </AnimatePresence>
    );
};

export { Tabs, TabList, Tab, TabPanels, TabPanel };

Props

Tabs

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside it_
defaultTabstringInitial active tab value when the component mounts_
className?stringOptional class names to style the Tabs component_
animationType?VariantsAnimation style for the active tab indicator_
instanceId?stringUnique identifier for the tabs instance for animation isolation_

Tablist

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside the Tablist_
className?stringOptional class names to style the Tablist_

Tab

PropTypeDescriptionDefault
childrenReactNodeContent displayed as the tab button label_
valuestringUnique identifier that maps this tab to its corresponding panel_
className?stringOptional class names to style the Tab_
icon?ReactNodeOptional icon element rendered before the tab label_

TabPanels

PropTypeDescriptionDefault
children?ReactNodeThe content to be rendered inside TabPanels_
className?stringOptional class names to style TabPanels_

TabPanel

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside TabPanel_
valuestringUnique value to distinguish the panel_
className?stringOptional class names to style the TabPanel_
variants?VariantsFramer motion variants_
transition??TransitionFramer motion transition_