Components

Vercel Tabs

A tab component with animated active and hover indicators, inspired by Vercel's dashboard UI.

Last updated on

Edit on GitHub
import { VercelTabs } from "@/components/ui/vercel-tabs";
 
const tabsData = [
  {
    label: "Overview",
    value: "Overview",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Overview Content</h2>
        <p className="text-muted-foreground">
          This is the content area for the Overview tab. You can pass any React
          components or content here.
        </p>
      </div>
    ),
  },
  {
    label: "Integrations",
    value: "Integrations",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Integrations</h2>
        <p className="text-muted-foreground">
          Connect your favorite tools and services.
        </p>
      </div>
    ),
  },
  {
    label: "Activity",
    value: "Activity",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Activity Log</h2>
        <p className="text-muted-foreground">
          View the latest activity on your account.
        </p>
      </div>
    ),
  },
  {
    label: "Domains",
    value: "Domains",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Domains</h2>
        <p className="text-muted-foreground">
          Manage your custom domains and DNS settings.
        </p>
      </div>
    ),
  },
  {
    label: "Usage",
    value: "Usage",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Usage Statistics</h2>
        <p className="text-muted-foreground">
          Check your resource usage and limits.
        </p>
      </div>
    ),
  },
  {
    label: "Monitoring",
    value: "Monitoring",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Monitoring</h2>
        <p className="text-muted-foreground">
          Real-time performance monitoring.
        </p>
      </div>
    ),
  },
];
 
export function VercelTabsDemo() {
  return (
    <div className="flex flex-col gap-8">
      <div className="flex flex-col gap-3">
        <h3 className="font-medium text-sm">Vercel Tabs</h3>
        <VercelTabs tabs={tabsData} defaultTab="Overview" />
      </div>
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://jolyui.dev/r/vercel-tabs"

Manual

Copy and paste the following code into your project component/ui/vercel-tabs.tsx

"use client";
 
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 
interface TabData {
  label: string;
  value: string;
  content: React.ReactNode;
}
 
interface VercelTabsProps {
  tabs: TabData[];
  defaultTab?: string;
  className?: string;
}
 
export function VercelTabs({ tabs, defaultTab, className }: VercelTabsProps) {
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
  const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.value);
  const [hoverStyle, setHoverStyle] = useState({});
  const [activeStyle, setActiveStyle] = useState({ left: "0px", width: "0px" });
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
 
  const activeIndex = tabs.findIndex((tab) => tab.value === activeTab);
 
  useEffect(() => {
    if (hoveredIndex !== null) {
      const hoveredElement = tabRefs.current[hoveredIndex];
      if (hoveredElement) {
        const { offsetLeft, offsetWidth } = hoveredElement;
        setHoverStyle({
          left: `${offsetLeft}px`,
          width: `${offsetWidth}px`,
        });
      }
    }
  }, [hoveredIndex]);
 
  useEffect(() => {
    const activeElement = tabRefs.current[activeIndex];
    if (activeElement) {
      const { offsetLeft, offsetWidth } = activeElement;
      setActiveStyle({
        left: `${offsetLeft}px`,
        width: `${offsetWidth}px`,
      });
    }
  }, [activeIndex]);
 
  useEffect(() => {
    requestAnimationFrame(() => {
      const activeElement = tabRefs.current[activeIndex];
      if (activeElement) {
        const { offsetLeft, offsetWidth } = activeElement;
        setActiveStyle({
          left: `${offsetLeft}px`,
          width: `${offsetWidth}px`,
        });
      }
    });
  }, [activeIndex]);
 
  return (
    <Tabs
      defaultValue={activeTab}
      onValueChange={setActiveTab}
      className={`flex w-full flex-col items-center ${className}`}
    >
      <TabsList className="relative h-auto select-none gap-[6px] bg-transparent p-0">
        {/* Hover Highlight */}
        <div
          className="absolute top-0 left-0 flex h-[30px] items-center rounded-[6px] bg-[#0e0f1114] transition-all duration-300 ease-out dark:bg-[#ffffff1a]"
          style={{
            ...hoverStyle,
            opacity: hoveredIndex !== null ? 1 : 0,
          }}
        />
 
        {/* Active Indicator */}
        <div
          className="absolute bottom-[-6px] h-[2px] bg-[#0e0f11] transition-all duration-300 ease-out dark:bg-white"
          style={activeStyle}
        />
 
        {tabs.map((tab, index) => (
          <TabsTrigger
            key={tab.value}
            value={tab.value}
            ref={(el) => {
              tabRefs.current[index] = el;
            }}
            className={`z-10 h-[30px] cursor-pointer rounded-md border-0 bg-transparent px-3 py-2 outline-none transition-colors duration-300 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 data-[state=active]:bg-transparent data-[state=active]:shadow-none ${
              activeTab === tab.value
                ? "text-[#0e0e10] dark:text-white"
                : "text-[#0e0f1199] dark:text-[#ffffff99]"
            }`}
            onMouseEnter={() => setHoveredIndex(index)}
            onMouseLeave={() => setHoveredIndex(null)}
          >
            <span className="whitespace-nowrap font-medium text-sm leading-5">
              {tab.label}
            </span>
          </TabsTrigger>
        ))}
      </TabsList>
 
      {/* Content Area */}
      <div className="mt-8 w-full px-4">
        {tabs.map((tab) => (
          <TabsContent
            key={tab.value}
            value={tab.value}
            className="fade-in-50 w-full animate-in duration-500"
          >
            {tab.content}
          </TabsContent>
        ))}
      </div>
    </Tabs>
  );
}

Usage

import { VercelTabs } from "@/components/ui/vercel-tabs";

const tabsData = [
  { label: "Overview", value: "Overview", content: <div>Overview Content</div> },
  { label: "Integrations", value: "Integrations", content: <div>Integrations Content</div> },
];

<VercelTabs tabs={tabsData} defaultTab="Overview" />

Examples

Basic

import { VercelTabs } from "@/components/ui/vercel-tabs";
 
const tabsData = [
  {
    label: "Overview",
    value: "Overview",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Overview Content</h2>
        <p className="text-muted-foreground">
          This is the content area for the Overview tab. You can pass any React
          components or content here.
        </p>
      </div>
    ),
  },
  {
    label: "Integrations",
    value: "Integrations",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Integrations</h2>
        <p className="text-muted-foreground">
          Connect your favorite tools and services.
        </p>
      </div>
    ),
  },
  {
    label: "Activity",
    value: "Activity",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Activity Log</h2>
        <p className="text-muted-foreground">
          View the latest activity on your account.
        </p>
      </div>
    ),
  },
  {
    label: "Domains",
    value: "Domains",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Domains</h2>
        <p className="text-muted-foreground">
          Manage your custom domains and DNS settings.
        </p>
      </div>
    ),
  },
  {
    label: "Usage",
    value: "Usage",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Usage Statistics</h2>
        <p className="text-muted-foreground">
          Check your resource usage and limits.
        </p>
      </div>
    ),
  },
  {
    label: "Monitoring",
    value: "Monitoring",
    content: (
      <div className="rounded-lg border border-gray-200 bg-white p-6 text-black dark:border-[#333] dark:bg-[#1c1c1c] dark:text-white">
        <h2 className="mb-2 font-bold text-2xl">Monitoring</h2>
        <p className="text-muted-foreground">
          Real-time performance monitoring.
        </p>
      </div>
    ),
  },
];
 
export function VercelTabsDemo() {
  return (
    <div className="flex flex-col gap-8">
      <div className="flex flex-col gap-3">
        <h3 className="font-medium text-sm">Vercel Tabs</h3>
        <VercelTabs tabs={tabsData} defaultTab="Overview" />
      </div>
    </div>
  );
}

API Reference

VercelTabs Props

Prop

Type

TabData Type

Prop

Type

Notes

  • Animated hover and active indicators
  • Fully customizable tab data and content
  • Uses Tailwind CSS for styling

How is this guide?

On this page