Tabs
Animated tabs with smooth transitions and motion highlight. Perfect for React applications requiring organized content sections with Next.js integration and TypeScript support.
Powered by
'use client';import { Button } from '@/components/ui/button';import { Input } from '@/components/ui/input';import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContents,} from '@/components/ui/shadcn-io/tabs';import { Label } from '@/components/ui/label';export default function TabsDemo() { return ( <Tabs defaultValue="account" className="w-[400px] bg-muted rounded-lg"> <TabsList className="grid w-full grid-cols-2"> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> </TabsList> <TabsContents className="mx-1 mb-1 -mt-2 rounded-sm h-full bg-background"> <TabsContent value="account" className="space-y-6 p-6"> <p className="text-sm text-muted-foreground"> Make changes to your account here. Click save when you're done. </p> <div className="space-y-3"> <div className="space-y-1"> <Label htmlFor="name">Name</Label> <Input id="name" defaultValue="Pedro Duarte" /> </div> <div className="space-y-1"> <Label htmlFor="username">Username</Label> <Input id="username" defaultValue="@peduarte" /> </div> </div> <Button>Save changes</Button> </TabsContent> <TabsContent value="password" className="space-y-6 p-6"> <p className="text-sm text-muted-foreground"> Change your password here. After saving, you'll be logged out. </p> <div className="space-y-3"> <div className="space-y-1"> <Label htmlFor="current">Current password</Label> <Input id="current" type="password" /> </div> <div className="space-y-1"> <Label htmlFor="new">New password</Label> <Input id="new" type="password" /> </div> <div className="space-y-1"> <Label htmlFor="confirm">Confirm password</Label> <Input id="confirm" type="password" /> </div> </div> <Button>Save password</Button> </TabsContent> </TabsContents> </Tabs> );}
'use client';import * as React from 'react';import { motion, type Transition, type HTMLMotionProps } from 'motion/react';import { cn } from '@/lib/utils';import { MotionHighlight, MotionHighlightItem,} from '@/components/ui/kibo-ui/motion-highlight';// Tabs Componenttype TabsContextType<T extends string> = { activeValue: T; handleValueChange: (value: T) => void; registerTrigger: (value: T, node: HTMLElement | null) => void;};// eslint-disable-next-line @typescript-eslint/no-explicit-anyconst TabsContext = React.createContext<TabsContextType<any> | undefined>( undefined,);function useTabs<T extends string = string>(): TabsContextType<T> { const context = React.useContext(TabsContext); if (!context) { throw new Error('useTabs must be used within a TabsProvider'); } return context;}type BaseTabsProps = React.ComponentProps<'div'> & { children: React.ReactNode;};type UnControlledTabsProps<T extends string = string> = BaseTabsProps & { defaultValue?: T; value?: never; onValueChange?: never;};type ControlledTabsProps<T extends string = string> = BaseTabsProps & { value: T; onValueChange?: (value: T) => void; defaultValue?: never;};type TabsProps<T extends string = string> = | UnControlledTabsProps<T> | ControlledTabsProps<T>;function Tabs<T extends string = string>({ defaultValue, value, onValueChange, children, className, ...props}: TabsProps<T>) { const [activeValue, setActiveValue] = React.useState<T | undefined>( defaultValue ?? undefined, ); const triggersRef = React.useRef(new Map<string, HTMLElement>()); const initialSet = React.useRef(false); const isControlled = value !== undefined; React.useEffect(() => { if ( !isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current ) { const firstTab = Array.from(triggersRef.current.keys())[0]; setActiveValue(firstTab as T); initialSet.current = true; } }, [activeValue, isControlled]); const registerTrigger = (value: string, node: HTMLElement | null) => { if (node) { triggersRef.current.set(value, node); if (!isControlled && activeValue === undefined && !initialSet.current) { setActiveValue(value as T); initialSet.current = true; } } else { triggersRef.current.delete(value); } }; const handleValueChange = (val: T) => { if (!isControlled) setActiveValue(val); else onValueChange?.(val); }; return ( <TabsContext.Provider value={{ activeValue: (value ?? activeValue)!, handleValueChange, registerTrigger, }} > <div data-slot="tabs" className={cn('flex flex-col gap-2', className)} {...props} > {children} </div> </TabsContext.Provider> );}type TabsListProps = React.ComponentProps<'div'> & { children: React.ReactNode; activeClassName?: string; transition?: Transition;};function TabsList({ children, className, activeClassName, transition = { type: 'spring', stiffness: 200, damping: 25, }, ...props}: TabsListProps) { const { activeValue } = useTabs(); return ( <MotionHighlight controlledItems className={cn('rounded-sm bg-background shadow-sm', activeClassName)} value={activeValue} transition={transition} > <div role="tablist" data-slot="tabs-list" className={cn( 'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]', className, )} {...props} > {children} </div> </MotionHighlight> );}type TabsTriggerProps = HTMLMotionProps<'button'> & { value: string; children: React.ReactNode;};function TabsTrigger({ ref, value, children, className, ...props}: TabsTriggerProps) { const { activeValue, handleValueChange, registerTrigger } = useTabs(); const localRef = React.useRef<HTMLButtonElement | null>(null); React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement); React.useEffect(() => { registerTrigger(value, localRef.current); return () => registerTrigger(value, null); }, [value, registerTrigger]); return ( <MotionHighlightItem value={value} className="size-full"> <motion.button ref={localRef} data-slot="tabs-trigger" role="tab" whileTap={{ scale: 0.95 }} onClick={() => handleValueChange(value)} data-state={activeValue === value ? 'active' : 'inactive'} className={cn( 'inline-flex cursor-pointer items-center size-full justify-center whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground z-[1]', className, )} {...props} > {children} </motion.button> </MotionHighlightItem> );}type TabsContentsProps = React.ComponentProps<'div'> & { children: React.ReactNode; transition?: Transition;};function TabsContents({ children, className, transition = { type: 'spring', stiffness: 300, damping: 30, bounce: 0, restDelta: 0.01, }, ...props}: TabsContentsProps) { const { activeValue } = useTabs(); const childrenArray = React.Children.toArray(children); const activeIndex = childrenArray.findIndex( (child): child is React.ReactElement<{ value: string }> => React.isValidElement(child) && typeof child.props === 'object' && child.props !== null && 'value' in child.props && child.props.value === activeValue, ); return ( <div data-slot="tabs-contents" className={cn('overflow-hidden', className)} {...props} > <motion.div className="flex -mx-2" animate={{ x: activeIndex * -100 + '%' }} transition={transition} > {childrenArray.map((child, index) => ( <div key={index} className="w-full shrink-0 px-2"> {child} </div> ))} </motion.div> </div> );}type TabsContentProps = HTMLMotionProps<'div'> & { value: string; children: React.ReactNode;};function TabsContent({ children, value, className, ...props}: TabsContentProps) { const { activeValue } = useTabs(); const isActive = activeValue === value; return ( <motion.div role="tabpanel" data-slot="tabs-content" className={cn('overflow-hidden', className)} initial={{ filter: 'blur(0px)' }} animate={{ filter: isActive ? 'blur(0px)' : 'blur(4px)' }} exit={{ filter: 'blur(0px)' }} transition={{ type: 'spring', stiffness: 200, damping: 25 }} {...props} > {children} </motion.div> );}export { Tabs, TabsList, TabsTrigger, TabsContents, TabsContent, useTabs, type TabsContextType, type TabsProps, type TabsListProps, type TabsTriggerProps, type TabsContentsProps, type TabsContentProps,};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/tabs.json
npx shadcn@latest add https://www.shadcn.io/registry/tabs.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/tabs.json
bunx shadcn@latest add https://www.shadcn.io/registry/tabs.json
Features
- Smooth animations with configurable spring transitions using Motion
- Motion highlight indicator that follows the active tab selection
- Controlled and uncontrolled modes for flexible state management
- TypeScript support with complete generic type definitions
- Blur effect transitions on content switching for visual polish
- Flexible layouts supporting horizontal and vertical orientations
Examples
Controlled Tabs
'use client';import * as React from 'react';import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContents,} from '@/components/ui/shadcn-io/tabs';export default function TabsControlledDemo() { const [value, setValue] = React.useState('tab1'); return ( <div className="space-y-4"> <div className="flex gap-2"> <button onClick={() => setValue('tab1')} className="px-3 py-1 text-sm border rounded-md" > Go to Tab 1 </button> <button onClick={() => setValue('tab2')} className="px-3 py-1 text-sm border rounded-md" > Go to Tab 2 </button> <button onClick={() => setValue('tab3')} className="px-3 py-1 text-sm border rounded-md" > Go to Tab 3 </button> </div> <Tabs value={value} onValueChange={setValue} className="w-[400px]"> <TabsList className="grid w-full grid-cols-3"> <TabsTrigger value="tab1">Tab 1</TabsTrigger> <TabsTrigger value="tab2">Tab 2</TabsTrigger> <TabsTrigger value="tab3">Tab 3</TabsTrigger> </TabsList> <TabsContents className="mt-2"> <TabsContent value="tab1" className="p-4"> <h3 className="font-semibold mb-2">First Tab</h3> <p className="text-muted-foreground"> This is the content of the first tab. The tab selection is controlled externally. </p> </TabsContent> <TabsContent value="tab2" className="p-4"> <h3 className="font-semibold mb-2">Second Tab</h3> <p className="text-muted-foreground"> This is the content of the second tab. Notice the smooth transition between tabs. </p> </TabsContent> <TabsContent value="tab3" className="p-4"> <h3 className="font-semibold mb-2">Third Tab</h3> <p className="text-muted-foreground"> This is the content of the third tab. The active tab state is managed by the parent component. </p> </TabsContent> </TabsContents> </Tabs> </div> );}
'use client';import * as React from 'react';import { motion, type Transition, type HTMLMotionProps } from 'motion/react';import { cn } from '@/lib/utils';import { MotionHighlight, MotionHighlightItem,} from '@/components/ui/kibo-ui/motion-highlight';// Tabs Componenttype TabsContextType<T extends string> = { activeValue: T; handleValueChange: (value: T) => void; registerTrigger: (value: T, node: HTMLElement | null) => void;};// eslint-disable-next-line @typescript-eslint/no-explicit-anyconst TabsContext = React.createContext<TabsContextType<any> | undefined>( undefined,);function useTabs<T extends string = string>(): TabsContextType<T> { const context = React.useContext(TabsContext); if (!context) { throw new Error('useTabs must be used within a TabsProvider'); } return context;}type BaseTabsProps = React.ComponentProps<'div'> & { children: React.ReactNode;};type UnControlledTabsProps<T extends string = string> = BaseTabsProps & { defaultValue?: T; value?: never; onValueChange?: never;};type ControlledTabsProps<T extends string = string> = BaseTabsProps & { value: T; onValueChange?: (value: T) => void; defaultValue?: never;};type TabsProps<T extends string = string> = | UnControlledTabsProps<T> | ControlledTabsProps<T>;function Tabs<T extends string = string>({ defaultValue, value, onValueChange, children, className, ...props}: TabsProps<T>) { const [activeValue, setActiveValue] = React.useState<T | undefined>( defaultValue ?? undefined, ); const triggersRef = React.useRef(new Map<string, HTMLElement>()); const initialSet = React.useRef(false); const isControlled = value !== undefined; React.useEffect(() => { if ( !isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current ) { const firstTab = Array.from(triggersRef.current.keys())[0]; setActiveValue(firstTab as T); initialSet.current = true; } }, [activeValue, isControlled]); const registerTrigger = (value: string, node: HTMLElement | null) => { if (node) { triggersRef.current.set(value, node); if (!isControlled && activeValue === undefined && !initialSet.current) { setActiveValue(value as T); initialSet.current = true; } } else { triggersRef.current.delete(value); } }; const handleValueChange = (val: T) => { if (!isControlled) setActiveValue(val); else onValueChange?.(val); }; return ( <TabsContext.Provider value={{ activeValue: (value ?? activeValue)!, handleValueChange, registerTrigger, }} > <div data-slot="tabs" className={cn('flex flex-col gap-2', className)} {...props} > {children} </div> </TabsContext.Provider> );}type TabsListProps = React.ComponentProps<'div'> & { children: React.ReactNode; activeClassName?: string; transition?: Transition;};function TabsList({ children, className, activeClassName, transition = { type: 'spring', stiffness: 200, damping: 25, }, ...props}: TabsListProps) { const { activeValue } = useTabs(); return ( <MotionHighlight controlledItems className={cn('rounded-sm bg-background shadow-sm', activeClassName)} value={activeValue} transition={transition} > <div role="tablist" data-slot="tabs-list" className={cn( 'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]', className, )} {...props} > {children} </div> </MotionHighlight> );}type TabsTriggerProps = HTMLMotionProps<'button'> & { value: string; children: React.ReactNode;};function TabsTrigger({ ref, value, children, className, ...props}: TabsTriggerProps) { const { activeValue, handleValueChange, registerTrigger } = useTabs(); const localRef = React.useRef<HTMLButtonElement | null>(null); React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement); React.useEffect(() => { registerTrigger(value, localRef.current); return () => registerTrigger(value, null); }, [value, registerTrigger]); return ( <MotionHighlightItem value={value} className="size-full"> <motion.button ref={localRef} data-slot="tabs-trigger" role="tab" whileTap={{ scale: 0.95 }} onClick={() => handleValueChange(value)} data-state={activeValue === value ? 'active' : 'inactive'} className={cn( 'inline-flex cursor-pointer items-center size-full justify-center whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground z-[1]', className, )} {...props} > {children} </motion.button> </MotionHighlightItem> );}type TabsContentsProps = React.ComponentProps<'div'> & { children: React.ReactNode; transition?: Transition;};function TabsContents({ children, className, transition = { type: 'spring', stiffness: 300, damping: 30, bounce: 0, restDelta: 0.01, }, ...props}: TabsContentsProps) { const { activeValue } = useTabs(); const childrenArray = React.Children.toArray(children); const activeIndex = childrenArray.findIndex( (child): child is React.ReactElement<{ value: string }> => React.isValidElement(child) && typeof child.props === 'object' && child.props !== null && 'value' in child.props && child.props.value === activeValue, ); return ( <div data-slot="tabs-contents" className={cn('overflow-hidden', className)} {...props} > <motion.div className="flex -mx-2" animate={{ x: activeIndex * -100 + '%' }} transition={transition} > {childrenArray.map((child, index) => ( <div key={index} className="w-full shrink-0 px-2"> {child} </div> ))} </motion.div> </div> );}type TabsContentProps = HTMLMotionProps<'div'> & { value: string; children: React.ReactNode;};function TabsContent({ children, value, className, ...props}: TabsContentProps) { const { activeValue } = useTabs(); const isActive = activeValue === value; return ( <motion.div role="tabpanel" data-slot="tabs-content" className={cn('overflow-hidden', className)} initial={{ filter: 'blur(0px)' }} animate={{ filter: isActive ? 'blur(0px)' : 'blur(4px)' }} exit={{ filter: 'blur(0px)' }} transition={{ type: 'spring', stiffness: 200, damping: 25 }} {...props} > {children} </motion.div> );}export { Tabs, TabsList, TabsTrigger, TabsContents, TabsContent, useTabs, type TabsContextType, type TabsProps, type TabsListProps, type TabsTriggerProps, type TabsContentsProps, type TabsContentProps,};
Manage tab state externally for programmatic control and synchronization with other components.
Vertical Layout
'use client';import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContents,} from '@/components/ui/shadcn-io/tabs';export default function TabsVerticalDemo() { return ( <Tabs defaultValue="overview" className="flex gap-4 w-full max-w-2xl"> <TabsList className="flex-col h-fit w-[200px]"> <TabsTrigger value="overview" className="w-full justify-start"> Overview </TabsTrigger> <TabsTrigger value="analytics" className="w-full justify-start"> Analytics </TabsTrigger> <TabsTrigger value="reports" className="w-full justify-start"> Reports </TabsTrigger> <TabsTrigger value="notifications" className="w-full justify-start"> Notifications </TabsTrigger> </TabsList> <TabsContents className="flex-1"> <TabsContent value="overview" className="p-6"> <h3 className="text-lg font-semibold mb-4">Overview</h3> <p className="text-muted-foreground mb-4"> Welcome to your dashboard. Here you can see a summary of your account activity and recent updates. </p> <div className="grid gap-4"> <div className="p-4 border rounded-lg"> <p className="text-sm font-medium">Total Users</p> <p className="text-2xl font-bold">1,234</p> </div> <div className="p-4 border rounded-lg"> <p className="text-sm font-medium">Active Sessions</p> <p className="text-2xl font-bold">89</p> </div> </div> </TabsContent> <TabsContent value="analytics" className="p-6"> <h3 className="text-lg font-semibold mb-4">Analytics</h3> <p className="text-muted-foreground"> View detailed analytics about your application performance and user behavior. </p> </TabsContent> <TabsContent value="reports" className="p-6"> <h3 className="text-lg font-semibold mb-4">Reports</h3> <p className="text-muted-foreground"> Generate and download custom reports based on your data. </p> </TabsContent> <TabsContent value="notifications" className="p-6"> <h3 className="text-lg font-semibold mb-4">Notifications</h3> <p className="text-muted-foreground"> Manage your notification preferences and view recent alerts. </p> </TabsContent> </TabsContents> </Tabs> );}
'use client';import * as React from 'react';import { motion, type Transition, type HTMLMotionProps } from 'motion/react';import { cn } from '@/lib/utils';import { MotionHighlight, MotionHighlightItem,} from '@/components/ui/kibo-ui/motion-highlight';// Tabs Componenttype TabsContextType<T extends string> = { activeValue: T; handleValueChange: (value: T) => void; registerTrigger: (value: T, node: HTMLElement | null) => void;};// eslint-disable-next-line @typescript-eslint/no-explicit-anyconst TabsContext = React.createContext<TabsContextType<any> | undefined>( undefined,);function useTabs<T extends string = string>(): TabsContextType<T> { const context = React.useContext(TabsContext); if (!context) { throw new Error('useTabs must be used within a TabsProvider'); } return context;}type BaseTabsProps = React.ComponentProps<'div'> & { children: React.ReactNode;};type UnControlledTabsProps<T extends string = string> = BaseTabsProps & { defaultValue?: T; value?: never; onValueChange?: never;};type ControlledTabsProps<T extends string = string> = BaseTabsProps & { value: T; onValueChange?: (value: T) => void; defaultValue?: never;};type TabsProps<T extends string = string> = | UnControlledTabsProps<T> | ControlledTabsProps<T>;function Tabs<T extends string = string>({ defaultValue, value, onValueChange, children, className, ...props}: TabsProps<T>) { const [activeValue, setActiveValue] = React.useState<T | undefined>( defaultValue ?? undefined, ); const triggersRef = React.useRef(new Map<string, HTMLElement>()); const initialSet = React.useRef(false); const isControlled = value !== undefined; React.useEffect(() => { if ( !isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current ) { const firstTab = Array.from(triggersRef.current.keys())[0]; setActiveValue(firstTab as T); initialSet.current = true; } }, [activeValue, isControlled]); const registerTrigger = (value: string, node: HTMLElement | null) => { if (node) { triggersRef.current.set(value, node); if (!isControlled && activeValue === undefined && !initialSet.current) { setActiveValue(value as T); initialSet.current = true; } } else { triggersRef.current.delete(value); } }; const handleValueChange = (val: T) => { if (!isControlled) setActiveValue(val); else onValueChange?.(val); }; return ( <TabsContext.Provider value={{ activeValue: (value ?? activeValue)!, handleValueChange, registerTrigger, }} > <div data-slot="tabs" className={cn('flex flex-col gap-2', className)} {...props} > {children} </div> </TabsContext.Provider> );}type TabsListProps = React.ComponentProps<'div'> & { children: React.ReactNode; activeClassName?: string; transition?: Transition;};function TabsList({ children, className, activeClassName, transition = { type: 'spring', stiffness: 200, damping: 25, }, ...props}: TabsListProps) { const { activeValue } = useTabs(); return ( <MotionHighlight controlledItems className={cn('rounded-sm bg-background shadow-sm', activeClassName)} value={activeValue} transition={transition} > <div role="tablist" data-slot="tabs-list" className={cn( 'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]', className, )} {...props} > {children} </div> </MotionHighlight> );}type TabsTriggerProps = HTMLMotionProps<'button'> & { value: string; children: React.ReactNode;};function TabsTrigger({ ref, value, children, className, ...props}: TabsTriggerProps) { const { activeValue, handleValueChange, registerTrigger } = useTabs(); const localRef = React.useRef<HTMLButtonElement | null>(null); React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement); React.useEffect(() => { registerTrigger(value, localRef.current); return () => registerTrigger(value, null); }, [value, registerTrigger]); return ( <MotionHighlightItem value={value} className="size-full"> <motion.button ref={localRef} data-slot="tabs-trigger" role="tab" whileTap={{ scale: 0.95 }} onClick={() => handleValueChange(value)} data-state={activeValue === value ? 'active' : 'inactive'} className={cn( 'inline-flex cursor-pointer items-center size-full justify-center whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground z-[1]', className, )} {...props} > {children} </motion.button> </MotionHighlightItem> );}type TabsContentsProps = React.ComponentProps<'div'> & { children: React.ReactNode; transition?: Transition;};function TabsContents({ children, className, transition = { type: 'spring', stiffness: 300, damping: 30, bounce: 0, restDelta: 0.01, }, ...props}: TabsContentsProps) { const { activeValue } = useTabs(); const childrenArray = React.Children.toArray(children); const activeIndex = childrenArray.findIndex( (child): child is React.ReactElement<{ value: string }> => React.isValidElement(child) && typeof child.props === 'object' && child.props !== null && 'value' in child.props && child.props.value === activeValue, ); return ( <div data-slot="tabs-contents" className={cn('overflow-hidden', className)} {...props} > <motion.div className="flex -mx-2" animate={{ x: activeIndex * -100 + '%' }} transition={transition} > {childrenArray.map((child, index) => ( <div key={index} className="w-full shrink-0 px-2"> {child} </div> ))} </motion.div> </div> );}type TabsContentProps = HTMLMotionProps<'div'> & { value: string; children: React.ReactNode;};function TabsContent({ children, value, className, ...props}: TabsContentProps) { const { activeValue } = useTabs(); const isActive = activeValue === value; return ( <motion.div role="tabpanel" data-slot="tabs-content" className={cn('overflow-hidden', className)} initial={{ filter: 'blur(0px)' }} animate={{ filter: isActive ? 'blur(0px)' : 'blur(4px)' }} exit={{ filter: 'blur(0px)' }} transition={{ type: 'spring', stiffness: 200, damping: 25 }} {...props} > {children} </motion.div> );}export { Tabs, TabsList, TabsTrigger, TabsContents, TabsContent, useTabs, type TabsContextType, type TabsProps, type TabsListProps, type TabsTriggerProps, type TabsContentsProps, type TabsContentProps,};
Create sidebar navigation patterns with vertically aligned tabs and content areas.
Use Cases
This free open source React component works well for:
- Settings panels - Organize configuration options into logical groups built with Next.js
- Profile pages - Separate user information, preferences, and activity using TypeScript and Tailwind CSS
- Documentation - Structure content with navigation between different sections
- Form wizards - Create multi-step forms with shadcn/ui integration
- Dashboard layouts - Switch between different data views and reports
API Reference
Tabs
Prop | Type | Default | Description |
---|---|---|---|
defaultValue | string | - | The default active tab (uncontrolled mode) |
value | string | - | The active tab value (controlled mode) |
onValueChange | (value: string) => void | - | Callback when tab changes |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | TabsList and TabsContents components |
TabsList
Prop | Type | Default | Description |
---|---|---|---|
activeClassName | string | - | Classes for the motion highlight element |
transition | Transition | { type: 'spring', stiffness: 200, damping: 25 } | Motion transition config |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | TabsTrigger components |
TabsTrigger
Prop | Type | Default | Description |
---|---|---|---|
value | string | required | Unique identifier for the tab |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Tab label content |
TabsContents
Prop | Type | Default | Description |
---|---|---|---|
transition | Transition | { type: 'spring', stiffness: 300, damping: 30 } | Content slide transition |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | TabsContent components |
TabsContent
Prop | Type | Default | Description |
---|---|---|---|
value | string | required | Matches TabsTrigger value |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Tab panel content |
Implementation Notes
- The component uses layout effects for accurate position calculations
- Motion highlight automatically tracks the active tab's position
- Content transitions use horizontal sliding with configurable spring physics
- Blur effects enhance the perception of content switching
- The first tab is automatically selected if no default is provided
- Compatible with shadcn/ui design system and form components
Stars Scrolling Wheel
Animated stars scrolling wheel with number counter and completion effects. Perfect for React applications requiring achievement displays with Next.js integration and TypeScript support.
Theme Switcher
Interactive theme toggle component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring system preference detection, smooth transitions, and accessibility features.