UIPackage

Tabs

React navigation
Edit on GitHub

Horizontal tab navigation with content panels — pick one panel at a time. Underline or pills variants. Built on Radix UI with full keyboard navigation.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://react.uipkge.dev/r/react/tabs.json

Or with the named registry: npx shadcn@latest add @uipkge-react/tabs

Examples

Props

Name Type / Values Default Required
variant segmented optional
orientation
'horizontal''vertical'
horizontal optional

Dependencies

Files (3)

  • components/ui/tabs/tabs.tsx 3.3 kB
    'use client'
    
    import * as React from 'react'
    import * as TabsPrimitive from '@radix-ui/react-tabs'
    import { cn } from '@/lib/utils'
    import { tabsListVariants, tabsTriggerVariants } from './tabs.variants'
    
    // Make orientation reachable from descendants without each consumer having to
    // pass it manually. TabsList / TabsTrigger read this to apply variant CSS.
    const TabsOrientationContext = React.createContext<'horizontal' | 'vertical'>('horizontal')
    
    export interface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {
      orientation?: 'horizontal' | 'vertical'
    }
    
    const Tabs = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Root>, TabsProps>(
      ({ className, orientation = 'horizontal', ...props }, ref) => (
        <TabsOrientationContext.Provider value={orientation}>
          <TabsPrimitive.Root
            ref={ref}
            data-uipkge=""
            data-slot="tabs"
            data-orientation={orientation}
            orientation={orientation}
            className={cn('flex w-full', orientation === 'vertical' ? 'flex-row gap-4' : 'flex-col gap-2', className)}
            {...props}
          />
        </TabsOrientationContext.Provider>
      ),
    )
    Tabs.displayName = 'Tabs'
    
    export interface TabsListProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {
      variant?: 'segmented' | 'pill' | 'underline'
      orientation?: 'horizontal' | 'vertical'
    }
    
    const TabsList = React.forwardRef<React.ElementRef<typeof TabsPrimitive.List>, TabsListProps>(
      ({ className, variant, orientation, ...props }, ref) => {
        const inherited = React.useContext(TabsOrientationContext)
        const effectiveOrientation = orientation ?? inherited
        return (
          <TabsPrimitive.List
            ref={ref}
            data-uipkge=""
            data-slot="tabs-list"
            className={cn(tabsListVariants({ variant, orientation: effectiveOrientation }), className)}
            {...props}
          />
        )
      },
    )
    TabsList.displayName = 'TabsList'
    
    export interface TabsTriggerProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {
      size?: 'default' | 'sm' | 'lg'
      variant?: 'segmented' | 'pill' | 'underline'
      orientation?: 'horizontal' | 'vertical'
    }
    
    const TabsTrigger = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Trigger>, TabsTriggerProps>(
      ({ className, size, variant, orientation, ...props }, ref) => {
        const inherited = React.useContext(TabsOrientationContext)
        const effectiveOrientation = orientation ?? inherited
        return (
          <TabsPrimitive.Trigger
            ref={ref}
            data-uipkge=""
            data-slot="tabs-trigger"
            className={cn(
              tabsTriggerVariants({ size, variant, orientation: effectiveOrientation }),
              className,
            )}
            {...props}
          />
        )
      },
    )
    TabsTrigger.displayName = 'TabsTrigger'
    
    const TabsContent = React.forwardRef<
      React.ElementRef<typeof TabsPrimitive.Content>,
      React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
    >(({ className, ...props }, ref) => (
      <TabsPrimitive.Content
        ref={ref}
        data-uipkge=""
        data-slot="tabs-content"
        className={cn(
          'ring-offset-background focus-visible:border-ring focus-visible:ring-ring/50 flex-1 focus-visible:ring-2 focus-visible:ring-[3px] focus-visible:outline-none',
          className,
        )}
        {...props}
      />
    ))
    TabsContent.displayName = 'TabsContent'
    
    export { Tabs, TabsList, TabsTrigger, TabsContent }
  • components/ui/tabs/tabs.variants.ts 2.9 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than the package
     * `index.ts`) so consuming Vue SFCs can import without creating a circular
     * dependency through the index. See card.variants.ts for the canonical
     * example + the SSR symptom that motivated the split.
     */
    
    export const tabsListVariants = cva('inline-flex items-stretch text-muted-foreground', {
      variants: {
        variant: {
          // Solid muted track with rounded inset triggers — the default look.
          segmented: 'gap-1 rounded-md bg-muted p-1',
          // Transparent track with rounded-full pill triggers.
          pill: 'gap-2 bg-transparent p-0',
          // Bottom-border bar (horizontal) or right-border bar (vertical), with
          // an underline on the active trigger.
          underline: 'gap-0 bg-transparent p-0',
        },
        orientation: {
          horizontal: 'flex-row',
          vertical: 'h-auto flex-col items-stretch',
        },
      },
      compoundVariants: [
        {
          variant: 'underline',
          orientation: 'horizontal',
          class: 'w-full justify-start border-b border-border',
        },
        {
          variant: 'underline',
          orientation: 'vertical',
          class: 'border-r border-border',
        },
      ],
      defaultVariants: {
        variant: 'segmented',
        orientation: 'horizontal',
      },
    })
    
    export const tabsTriggerVariants = cva(
      'inline-flex items-center justify-center gap-1.5 whitespace-nowrap font-medium ring-offset-background transition-[color,background-color,box-shadow,border-color] duration-150 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
      {
        variants: {
          variant: {
            segmented:
              'rounded-sm data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs',
            pill: 'rounded-full border border-border bg-transparent data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:border-primary',
            underline:
              'rounded-none border-b-2 border-transparent -mb-px data-[state=active]:border-foreground data-[state=active]:text-foreground',
          },
          size: {
            default: 'h-9 px-3 text-sm',
            sm: 'h-8 px-2.5 text-xs',
            lg: 'h-10 px-4 text-sm',
          },
          orientation: {
            horizontal: '',
            vertical: 'w-full justify-start',
          },
        },
        compoundVariants: [
          {
            variant: 'underline',
            orientation: 'vertical',
            class: 'border-b-0 border-r-2 -mr-px',
          },
        ],
        defaultVariants: {
          variant: 'segmented',
          size: 'default',
          orientation: 'horizontal',
        },
      },
    )
    
    export type TabsListVariants = VariantProps<typeof tabsListVariants>
    
    export type TabsTriggerVariants = VariantProps<typeof tabsTriggerVariants>
  • components/ui/tabs/index.ts 0.4 kB
    export {
      Tabs,
      TabsList,
      TabsTrigger,
      TabsContent,
      type TabsProps,
      type TabsListProps,
      type TabsTriggerProps,
    } from './tabs'
    
    // Re-export variant API from the sibling file (kept separate to avoid the
    // component <-> index.ts circular import that broke dev SSR for Card).
    export {
      tabsListVariants,
      tabsTriggerVariants,
      type TabsListVariants,
      type TabsTriggerVariants,
    } from './tabs.variants'

Raw manifest: https://react.uipkge.dev/r/react/tabs.json