Navs & Tabs
Tab navigation with underline indicator, context-driven active state, and composable tab groups with panels for organized content switching.
Preview
Basic tabs
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@/registry/ui-kit/tailwind-navs-tabs/react'
function Example() {
return (
<TabGroup>
<TabList>
<Tab index={0}>Account</Tab>
<Tab index={1}>Notifications</Tab>
<Tab index={2}>Billing</Tab>
</TabList>
<TabPanels>
<TabPanel index={0}>
<p>Account settings content here.</p>
</TabPanel>
<TabPanel index={1}>
<p>Notification preferences content here.</p>
</TabPanel>
<TabPanel index={2}>
<p>Billing and plan details here.</p>
</TabPanel>
</TabPanels>
</TabGroup>
)
}Component API
| Prop | Default | Description |
|---|---|---|
TabGroup | — | State wrapper that manages active tab index via React context. |
defaultIndex | 0 | Initial active tab index. |
TabList | — | Container for tab buttons with bottom border. |
Tab | — | Tab button with active underline indicator. Requires index prop. |
index | — | Zero-based index identifying the tab and its corresponding panel. |
TabPanels | — | Container for tab panel content. |
TabPanel | — | Content panel shown when its index matches the active tab. Requires index prop. |
Source Code
"use client";
import { createContext, useContext, useState } from "react";
/* ── Context ── */
interface TabContextValue {
activeIndex: number;
setActiveIndex: (index: number) => void;
}
const TabContext = createContext<TabContextValue>({
activeIndex: 0,
setActiveIndex: () => {},
});
/* ── TabGroup ── */
interface TabGroupProps {
children: React.ReactNode;
defaultIndex?: number;
className?: string;
}
function TabGroup({ children, defaultIndex = 0, className = "" }: TabGroupProps) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className={className}>{children}</div>
</TabContext.Provider>
);
}
/* ── TabList ── */
interface TabListProps {
children: React.ReactNode;
className?: string;
}
function TabList({ children, className = "" }: TabListProps) {
return (
<div
className={`flex gap-1 border-b border-zinc-200 dark:border-zinc-800 ${className}`}
role="tablist"
>
{children}
</div>
);
}
/* ── Tab ── */
interface TabProps {
children: React.ReactNode;
index: number;
className?: string;
}
function Tab({ children, index, className = "" }: TabProps) {
const { activeIndex, setActiveIndex } = useContext(TabContext);
const isActive = activeIndex === index;
return (
<button
role="tab"
type="button"
aria-selected={isActive}
className={`relative px-4 py-2.5 text-sm font-medium transition-colors ${
isActive
? "text-indigo-600 dark:text-indigo-400 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-indigo-600 dark:after:bg-indigo-400"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200"
} ${className}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
/* ── TabPanels ── */
interface TabPanelsProps {
children: React.ReactNode;
className?: string;
}
function TabPanels({ children, className = "" }: TabPanelsProps) {
return <div className={className}>{children}</div>;
}
/* ── TabPanel ── */
interface TabPanelProps {
children: React.ReactNode;
index: number;
className?: string;
}
function TabPanel({ children, index, className = "" }: TabPanelProps) {
const { activeIndex } = useContext(TabContext);
if (activeIndex !== index) return null;
return (
<div role="tabpanel" className={`py-4 ${className}`}>
{children}
</div>
);
}
export { TabGroup, TabList, Tab, TabPanels, TabPanel };
export default TabGroup;