Tab Switcher
Tabs-based variant switcher utility.
Installation
CLI
npx shadcn@latest add "https://ui.xaclabs.dev/r/tab-switcher.json"Usage
import { TabSwitcher } from "@/registry/components/tab-switcher"View component source
tab-switcher.tsx
tsx"use client"
import { AnimatePresence, motion } from "motion/react"
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
interface TabSwitcherItem {
value: string
label: string
content: React.ReactNode
}
export interface TabSwitcherProps {
queryKey?: string
items: TabSwitcherItem[]
defaultValue?: string
className?: string
}
function getQueryParam(key: string): string | null {
if (typeof window === "undefined") return null
const params = new URLSearchParams(window.location.search)
return params.get(key)
}
function setQueryParam(key: string, value: string) {
const url = new URL(window.location.href)
url.searchParams.set(key, value)
window.history.replaceState({}, "", url.toString())
}
export function TabSwitcher({
queryKey = "tab",
items,
defaultValue,
className,
}: TabSwitcherProps) {
const fallback = defaultValue || items[0]?.value || ""
const [value, setValue] = React.useState(fallback)
React.useEffect(() => {
const fromUrl = getQueryParam(queryKey)
if (fromUrl && items.some((item) => item.value === fromUrl)) {
setValue(fromUrl)
}
}, [queryKey, items])
React.useEffect(() => {
const handler = () => {
const fromUrl = getQueryParam(queryKey)
if (fromUrl && items.some((item) => item.value === fromUrl)) {
setValue(fromUrl)
}
}
window.addEventListener("tab-switcher-sync", handler)
return () => window.removeEventListener("tab-switcher-sync", handler)
}, [queryKey, items])
const handleChange = React.useCallback(
(newValue: string) => {
setValue(newValue)
setQueryParam(queryKey, newValue)
window.dispatchEvent(new CustomEvent("tab-switcher-sync"))
},
[queryKey],
)
return (
<Tabs value={value} onValueChange={handleChange} className={className}>
<TabsList>
{items.map((item) => (
<TabsTrigger key={item.value} value={item.value} className="relative">
{item.label}
{value === item.value && (
<motion.div
layoutId="active-tab-indicator"
className="absolute bottom-0 left-0 h-[2px] w-full bg-foreground"
transition={{
type: "spring",
stiffness: 500,
damping: 30,
}}
/>
)}
</TabsTrigger>
))}
</TabsList>
{items.map((item) => (
<TabsContent key={item.value} value={item.value}>
<AnimatePresence mode="wait">
<motion.div
key={item.value}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
{item.content}
</motion.div>
</AnimatePresence>
</TabsContent>
))}
</Tabs>
)
}