Shared Layout Tabs
Tabs whose indicator moves via a shared layout transition.
npx shadcn@latest add https://noechague-site.vercel.app/r/shared-layout-tabs.jsonshared-layout-tabs.tsx
"use client"
import { useId, useState } from "react"
import { motion, useReducedMotion } from "motion/react"
import { Tabs } from "@base-ui/react/tabs"
import { cn } from "@/lib/utils"
export function SharedLayoutTabs({
tabs,
defaultTab,
panelId,
onChange,
}: {
tabs: string[]
defaultTab?: string
/** id of the controlled panel. Without it `aria-controls` is omitted rather than left empty. */
panelId?: string
onChange?: (tab: string) => void
}) {
const [active, setActive] = useState(defaultTab ?? tabs[0])
const layoutId = useId()
const reduced = useReducedMotion() ?? false
return (
<Tabs.Root
onValueChange={(value: string) => {
setActive(value)
onChange?.(value)
}}
value={active}
>
{/* The tablist role, roving tabindex, wrapping arrow keys and Home/End
all come from Base UI, which is tested against real assistive
technology. This is not a hand-rolled reimplementation.
`activateOnFocus`: by default the primitive only moves focus and waits
for Enter or Space. We want an arrow key to select the tab outright. */}
<Tabs.List
activateOnFocus
className="flex items-center gap-1 rounded-full bg-neutral-100 p-1"
>
{tabs.map((tab) => {
const selected = tab === active
return (
<Tabs.Tab
aria-controls={panelId}
// Stock Tailwind classes only: this file is installed into other
// people's projects, where the site's own scale (gray-1000,
// preview-bg, shadow-custom) and the `hover-hover` variant do not
// exist. They generate no CSS there, so the indicator would lose
// its background and the selected tab its colour, silently.
className={cn(
"relative flex h-9 cursor-pointer items-center justify-center rounded-full px-4",
"font-medium text-sm transition-colors duration-200 ease-out",
selected ? "text-neutral-900" : "text-neutral-600 hover:text-neutral-900",
)}
key={tab}
value={tab}
>
{selected && (
<motion.span
className="absolute inset-0 rounded-full bg-white shadow-sm"
layoutId={layoutId}
transition={
reduced ? { duration: 0 } : { type: "spring", duration: 0.4, bounce: 0.18 }
}
/>
)}
<span className="relative z-10">{tab}</span>
</Tabs.Tab>
)
})}
</Tabs.List>
</Tabs.Root>
)
}