// shadcn Tabs — CSS-only with the :target trick OR a tiny client // state hook. V1 ships a controlled component using React state; // island-eligible. Apps wanting zero-JS tabs use the :target form // (anchor-based) which we don't ship as a primitive yet. import { createContext, useContext, useState, type HTMLAttributes, type ReactNode, } from 'react' import { cn } from '../cn' interface TabsContextValue { readonly value: string readonly setValue: (v: string) => void } const TabsContext = createContext(null) interface TabsProps extends HTMLAttributes { readonly defaultValue: string readonly value?: string readonly onValueChange?: (v: string) => void } export const Tabs = ({ defaultValue, value, onValueChange, className, children, ...props }: TabsProps): ReactNode => { const [uncontrolled, setUncontrolled] = useState(defaultValue) const current = value ?? uncontrolled const setValue = (v: string): void => { if (value === undefined) setUncontrolled(v) onValueChange?.(v) } return (
{children}
) } export const TabsList = ({ className, ...props }: HTMLAttributes): ReactNode => (
) interface TabsTriggerProps extends HTMLAttributes { readonly value: string } export const TabsTrigger = ({ value, className, children, ...props }: TabsTriggerProps): ReactNode => { const ctx = useContext(TabsContext) if (!ctx) throw new Error('TabsTrigger must be inside ') const active = ctx.value === value return ( ) } interface TabsContentProps extends HTMLAttributes { readonly value: string } export const TabsContent = ({ value, className, children, ...props }: TabsContentProps): ReactNode => { const ctx = useContext(TabsContext) if (!ctx) throw new Error('TabsContent must be inside ') if (ctx.value !== value) return null return (
{children}
) }