✨ Introducing NextLaunch — Premium templates designed to help you build stunning landing pages in minutes.

Tabs

A fully accessible and customizable tabbed interface component with support for controlled and uncontrolled modes.

Basic Example

Content for Tab 1

This is the content area for tab 1. You can put any content here.

Installation

1. Dependencies

npm install clsx tailwind-merge

2. Paste inside @/lib/utils.ts

import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: Parameters<typeof clsx>) {
  return twMerge(clsx(...inputs));
}

3. Copy Tabs Code

'use client';

import React, { createContext, useContext, useState, HTMLAttributes } from "react";
import { cn } from "@/lib/utils";

// Define interfaces for our components
interface TabsContextValue {
  value: string;
  onValueChange: (value: string) => void;
}

interface TabsProps extends HTMLAttributes<HTMLDivElement> {
  defaultValue?: string;
  value?: string;
  onValueChange?: (value: string) => void;
  children: React.ReactNode;
}

interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  value: string;
}

interface TabsContentProps extends HTMLAttributes<HTMLDivElement> {
  value: string;
}

// Create context for tabs
const TabsContext = createContext<TabsContextValue | undefined>(undefined);

const useTabs = () => {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error("Tabs components must be used within a Tabs provider");
  }
  return context;
};

const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
  ({ className, defaultValue, value: controlledValue, onValueChange, children, ...props }, ref) => {
    // Use controlled or uncontrolled state
    const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue || "");
    
    // Determine if the component is controlled or uncontrolled
    const isControlled = controlledValue !== undefined;
    const value = isControlled ? controlledValue : uncontrolledValue;
    
    // Handle value change
    const handleValueChange = (newValue: string) => {
      if (!isControlled) {
        setUncontrolledValue(newValue);
      }
      onValueChange?.(newValue);
    };

    return (
      <TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
        <div
          className={cn(
            "w-full h-full flex flex-col justify-center items-start gap-4",
            className
          )}
          ref={ref}
          {...props}
        >
          {children}
        </div>
      </TabsContext.Provider>
    );
  }
);

Tabs.displayName = "Tabs";

const TabsList = React.forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      className={cn(
        "inline-flex justify-center items-center gap-1 h-9 bg-neutral-700 rounded-sm p-1",
        className
      )}
      ref={ref}
      {...props}
    />
  )
);

TabsList.displayName = "TabsList";

const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
  ({ className, value, ...props }, ref) => {
    const { value: selectedValue, onValueChange } = useTabs();
    const isActive = selectedValue === value;

    return (
      <button
        className={cn(
          "inline-flex justify-center items-center line-clamp-1 whitespace-nowrap overflow-hidden px-3 py-1 text-sm transition-colors rounded-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
          isActive 
            ? "bg-neutral-800 text-white" 
            : "bg-transparent text-neutral-200 hover:bg-neutral-500",
          className
        )}
        onClick={() => onValueChange(value)}
        role="tab"
        aria-selected={isActive}
        data-state={isActive ? "active" : "inactive"}
        ref={ref}
        {...props}
      />
    );
  }
);

TabsTrigger.displayName = "TabsTrigger";

const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
  ({ className, value, children, ...props }, ref) => {
    const { value: selectedValue } = useTabs();
    const isActive = selectedValue === value;

    if (!isActive) return null;

    return (
      <div
        className={cn(
          "w-full h-full bg-neutral-700 rounded-sm p-2",
          className
        )}
        role="tabpanel"
        data-state={isActive ? "active" : "inactive"}
        ref={ref}
        {...props}
      >
        {children}
      </div>
    );
  }
);

TabsContent.displayName = "TabsContent";

export { Tabs, TabsList, TabsTrigger, TabsContent };

Copy Tabs Code

Tabs Props

NameTypeRequiredDefaultDescription
defaultValuestringNoundefinedThe default value for the uncontrolled tabs. Represents the initially selected tab.
valuestringNoundefinedThe controlled value for the tabs. When provided, the component operates in controlled mode.
onValueChange(value: string) => voidNoundefinedCallback function that fires when the selected tab changes. Required for controlled usage.
childrenReact.ReactNodeYesundefinedThe content of the tabs component, typically includes TabsList and TabsContent components.
classNamestringNoundefinedAdditional class names for custom styling of the tabs container.

TabsList Props

NameTypeRequiredDefaultDescription
childrenReact.ReactNodeYesundefinedThe content of the tabs list, typically includes TabsTrigger components.
classNamestringNoundefinedAdditional class names for custom styling of the tabs list container.

TabsTrigger Props

NameTypeRequiredDefaultDescription
valuestringYesundefinedThe unique identifier for the tab that matches with the corresponding TabsContent value.
childrenReact.ReactNodeNoundefinedThe content of the tab trigger button, such as text or icons.
classNamestringNoundefinedAdditional class names for custom styling of the tab trigger button.
disabledbooleanNofalseWhether the tab trigger is disabled.

TabsContent Props

NameTypeRequiredDefaultDescription
valuestringYesundefinedThe unique identifier for the content that matches with the corresponding TabsTrigger value.
childrenReact.ReactNodeNoundefinedThe content to display when the tab is active.
classNamestringNoundefinedAdditional class names for custom styling of the tab content.