Command Palette

Search for a command to run...

Tabs

Tabs with a mouse-sensitive spotlight effect that highlights the border of the div or section as you move your cursor

design
collaborate
share
publish
preview_img

Installation

Install the following dependencies:

pnpm add motion

Create a file at components/motion/tabs-rc.tsx and paste the given code.

"use client";
 
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import React, {
    ReactNode,
    createContext,
    useContext,
    useEffect,
    useState,
    isValidElement,
} from "react";
 
/*eslint-disable @typescript-eslint/no-explicit-any*/
interface TabContextType {
    activeTab: string;
    setActiveTab: (value: string) => void;
    wobbly: boolean;
    hover: boolean;
    defaultValue: string;
    prevIndex: number;
    setPrevIndex: (value: number) => void;
    tabsOrder: string[];
}
const TabContext = createContext<TabContextType | undefined>(undefined);
 
export const useTabs = () => {
    const context = useContext(TabContext);
    if (!context) {
        throw new Error("useTabs must be used within a TabsProvider");
    }
    return context;
};
 
interface TabsProviderProps {
    children: ReactNode;
    defaultValue: string;
    wobbly?: boolean;
    hover?: boolean;
}
 
export const TabsProvider = ({
    children,
    defaultValue,
    wobbly = true,
    hover = false,
}: TabsProviderProps) => {
    const [activeTab, setActiveTab] = useState(defaultValue);
    const [prevIndex, setPrevIndex] = useState(0);
    const [tabsOrder, setTabsOrder] = useState<string[]>([]);
    useEffect(() => {
        const order: string[] = [];
        React.Children.toArray(children).forEach((child) => {
            if (isValidElement(child) && child.type === TabsContent) {
                const typedChild = child as React.ReactElement<{ value: string }>;
                order.push(typedChild.props.value);
            }
        });
        setTabsOrder(order);
    }, [children]);
 
    return (
        <TabContext.Provider
            value={{
                activeTab,
                setActiveTab,
                wobbly,
                hover,
                defaultValue,
                setPrevIndex,
                prevIndex,
                tabsOrder,
            }}
        >
            {children}
        </TabContext.Provider>
    );
};
 
export const TabsBtn = ({ children, className, value }: any) => {
    const {
        activeTab,
        setPrevIndex,
        setActiveTab,
        defaultValue,
        hover,
        wobbly,
        tabsOrder,
    } = useTabs();
 
    const handleClick = () => {
        setPrevIndex(tabsOrder.indexOf(activeTab));
        setActiveTab(value);
    };
 
    return (
        <>
            <>
                <motion.div
                    className={cn(
                        `relative cursor-pointer rounded-md p-1 px-2 sm:p-2 sm:px-4`,
                        className,
                    )}
                    onFocus={() => {
                        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
                        hover && handleClick();
                    }}
                    onMouseEnter={() => {
                        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
                        hover && handleClick();
                    }}
                    onClick={handleClick}
                >
                    {children}
 
                    {activeTab === value && (
                        <AnimatePresence mode="wait">
                            <motion.div
                                transition={{
                                    layout: {
                                        duration: 0.2,
                                        ease: "easeInOut",
                                        delay: 0.2,
                                    },
                                }}
                                layoutId={defaultValue}
                                className="dark:bg-primary-base absolute top-0 left-0 z-[1] h-full w-full rounded-md bg-white"
                            />
                        </AnimatePresence>
                    )}
 
                    {wobbly ? (
                        <>
                            {activeTab === value && (
                                <AnimatePresence mode="wait">
                                    <motion.div
                                        transition={{
                                            layout: {
                                                duration: 0.4,
                                                ease: "easeInOut",
                                                delay: 0.04,
                                            },
                                        }}
                                        layoutId={defaultValue}
                                        className="dark:bg-primary-base tab-shadow absolute top-0 left-0 z-[1] h-full w-full rounded-md bg-white"
                                    />
                                </AnimatePresence>
                            )}
                            {activeTab === value && (
                                <AnimatePresence mode="wait">
                                    <motion.div
                                        transition={{
                                            layout: {
                                                duration: 0.4,
                                                ease: "easeOut",
                                                delay: 0.2,
                                            },
                                        }}
                                        layoutId={`${defaultValue}b`}
                                        className="dark:bg-primary-base tab-shadow absolute top-0 left-0 z-[1] h-full w-full rounded-md bg-white"
                                    />
                                </AnimatePresence>
                            )}
                        </>
                    ) : (
                        <></>
                    )}
                </motion.div>
            </>
        </>
    );
};
 
export const TabsContent = ({ children, className, value, yValue }: any) => {
    const { activeTab, tabsOrder, prevIndex } = useTabs();
    const isForward = tabsOrder.indexOf(activeTab) > prevIndex;
    return (
        <>
            <AnimatePresence mode="popLayout">
                {activeTab === value && (
                    <motion.div
                        initial={{
                            opacity: 0,
                            y: yValue ? (isForward ? 10 : -10) : 0,
                        }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{
                            opacity: 0,
                            y: yValue ? (isForward ? -50 : 50) : 0,
                        }}
                        transition={{
                            duration: 0.3,
                            ease: "easeInOut",
                            delay: 0.5,
                        }}
                        className={cn(
                            "relative rounded-md p-2 px-4",
                            className,
                        )}
                    >
                        {activeTab === value ? children : null}
                    </motion.div>
                )}
            </AnimatePresence>
        </>
    );
};
 
/*eslint-enable @typescript-eslint/no-explicit-any*/

Update the import paths to match your project setup.