Tabs

Tabbed content panels with keyboard-accessible tab triggers.

Manage your account here.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.com/r/tabs.json
bash

Code

"use client";

import { createContext, useContext, useMemo, useState } from "react";

import type { ReactNode } from "react";

import { cn } from "../../lib/utils";

// Context for tabs state
type TabsContextValue = {
  activeTab: string;
  setActiveTab: (value: string) => void;
};

const TabsContext = createContext<null | TabsContextValue>(null);

function useTabsContext(): TabsContextValue {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error("Tab components must be used within a Tabs component");
  }
  return context;
}

export type TabsProps = {
  children: ReactNode;
  className?: string;
  defaultValue: string;
  onValueChange?: (value: string) => void;
};

function Tabs({
  children,
  className,
  defaultValue,
  onValueChange,
}: TabsProps): React.ReactNode {
  const [activeTab, setActiveTab] = useState(defaultValue);

  const handleSetActiveTab = (value: string): void => {
    setActiveTab(value);
    onValueChange?.(value);
  };

  const contextValue = useMemo(
    () => ({ activeTab, setActiveTab: handleSetActiveTab }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [activeTab],
  );

  return (
    <TabsContext.Provider value={contextValue}>
      <div className={cn("my-6", className)}>{children}</div>
    </TabsContext.Provider>
  );
}

export type TabsListProps = {
  children: ReactNode;
  className?: string;
};

function TabsList({ children, className }: TabsListProps): React.ReactNode {
  return (
    <div
      className={cn("flex border-b border-border overflow-x-auto", className)}
      role="tablist"
    >
      {children}
    </div>
  );
}

export type TabsTriggerProps = {
  children: ReactNode;
  className?: string;
  value: string;
};

function TabsTrigger({
  children,
  className,
  value,
}: TabsTriggerProps): React.ReactNode {
  const { activeTab, setActiveTab } = useTabsContext();
  const isActive = activeTab === value;

  return (
    <button
      aria-selected={isActive}
      className={cn(
        "px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors",
        "border-b-2 -mb-px",
        isActive
          ? "border-primary text-primary"
          : "border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground/50",
        className,
      )}
      onClick={() => {
        setActiveTab(value);
      }}
      role="tab"
      type="button"
    >
      {children}
    </button>
  );
}

export type TabsContentProps = {
  children: ReactNode;
  className?: string;
  value: string;
};

function TabsContent({
  children,
  className,
  value,
}: TabsContentProps): React.ReactNode {
  const { activeTab } = useTabsContext();

  if (activeTab !== value) return null;

  return (
    <div className={cn("pt-4", className)} role="tabpanel">
      {children}
    </div>
  );
}

// Attach sub-components
Tabs.List = TabsList;
Tabs.Trigger = TabsTrigger;
Tabs.Content = TabsContent;

export { Tabs, TabsContent, TabsList, TabsTrigger };
typescript