UIPackage
Menu

Bottom Navigation

bottom-navigation ui
Edit on GitHub

Mobile bottom tab bar with icon + label items. Supports value/defaultValue for the active item, an active color, fixed positioning at the viewport bottom, badges on items, and a `to` prop for link integration.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/bottom-navigation.json
Named registry: npx shadcn@latest add @uipkge-react/bottom-navigation Installs to: components/ui/bottom-navigation/

Examples

Props

Name Type / Values Default Required
items

Tab items.

BottomNavItem[] required
value

Active item value (controlled).

string optional
defaultValue

Initial active item value (uncontrolled).

string optional
activeColor

Active item color — a Tailwind text color class. Default 'text-primary'.

string optional
fixed

Fixed positioning at the viewport bottom. Default true.

boolean optional
showIndicator

Show an active indicator pill behind the icon. Default true.

boolean optional
safeArea

Safe-area padding for notched devices (iOS). Default true.

boolean optional
onValueChange

Called when the active item changes.

(value: string) => void optional
onSelect

Called when an item is selected, receiving the full item.

(item: BottomNavItem) => void optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

BottomNavItem
interface BottomNavItem {
  /** Unique value identifying this tab. Used with value/defaultValue. */
  value: string
  /** Label shown under the icon. */
  label: string
  /** Lucide icon component. */
  icon: LucideIcon
  /** Optional badge count or text shown on the icon. */
  badge?: string | number
  /** Link destination (renders an anchor instead of a button). */
  to?: string
}

npm dependencies

Files installed (2)

  • components/ui/bottom-navigation/BottomNavigation.tsx 5.2 kB
    import * as React from 'react'
    import type { LucideIcon } from 'lucide-react'
    import { cn } from '@/lib/utils'
    
    export interface BottomNavItem {
      /** Unique value identifying this tab. Used with value/defaultValue. */
      value: string
      /** Label shown under the icon. */
      label: string
      /** Lucide icon component. */
      icon: LucideIcon
      /** Optional badge count or text shown on the icon. */
      badge?: string | number
      /** Link destination (renders an anchor instead of a button). */
      to?: string
    }
    
    export interface BottomNavigationProps extends React.HTMLAttributes<HTMLElement> {
      /** Tab items. */
      items: BottomNavItem[]
      /** Active item value (controlled). */
      value?: string
      /** Initial active item value (uncontrolled). */
      defaultValue?: string
      /** Active item color — a Tailwind text color class. Default 'text-primary'. */
      activeColor?: string
      /** Fixed positioning at the viewport bottom. Default true. */
      fixed?: boolean
      /** Show an active indicator pill behind the icon. Default true. */
      showIndicator?: boolean
      /** Safe-area padding for notched devices (iOS). Default true. */
      safeArea?: boolean
      /** Called when the active item changes. */
      onValueChange?: (value: string) => void
      /** Called when an item is selected, receiving the full item. */
      onSelect?: (item: BottomNavItem) => void
    }
    
    const BottomNavigation = React.forwardRef<HTMLElement, BottomNavigationProps>(
      (
        {
          className,
          items,
          value,
          defaultValue = '',
          activeColor = 'text-primary',
          fixed = true,
          showIndicator = true,
          safeArea = true,
          onValueChange,
          onSelect,
          ...props
        },
        ref,
      ) => {
        const isControlled = value !== undefined
        const [internal, setInternal] = React.useState(defaultValue)
        const active = isControlled ? value : internal
    
        const handleSelect = (item: BottomNavItem) => {
          if (!isControlled) setInternal(item.value)
          onValueChange?.(item.value)
          onSelect?.(item)
        }
    
        return (
          <nav
            data-uipkge=""
            data-slot="bottom-navigation"
            data-fixed={fixed ? '' : undefined}
            ref={ref as React.Ref<HTMLElement>}
            className={cn(
              'border-border bg-background/95 z-50 flex items-stretch justify-around border-t backdrop-blur-sm',
              fixed ? 'fixed inset-x-0 bottom-0' : 'relative',
              safeArea && 'pb-[env(safe-area-inset-bottom)]',
              className,
            )}
            {...props}
          >
            {items.map((item) => {
              const isActive = active === item.value
              const Icon = item.icon
              const itemClass = cn(
                'focus-visible:ring-ring/50 relative flex flex-1 flex-col items-center justify-center gap-0.5 pt-2 pb-1.5 text-xs transition-colors duration-200 outline-none focus-visible:ring-[3px]',
                isActive ? activeColor : 'text-muted-foreground hover:text-foreground',
              )
              const inner = (
                <>
                  {showIndicator && isActive ? (
                    <span
                      className="bg-primary/10 absolute top-1 h-8 w-14 rounded-full transition-opacity duration-200"
                      aria-hidden="true"
                    />
                  ) : null}
                  <span className="relative flex items-center justify-center">
                    <Icon
                      className={cn('size-5 transition-transform duration-200', isActive && 'scale-110')}
                    />
                    {item.badge !== undefined && item.badge !== '' ? (
                      <span className="bg-destructive text-destructive-foreground absolute -top-1.5 -right-2 flex min-w-4 items-center justify-center rounded-full px-1 text-[10px] leading-4 font-medium">
                        {item.badge}
                      </span>
                    ) : null}
                  </span>
                  <span
                    className={cn(
                      'max-w-full truncate px-1 transition-opacity duration-200',
                      isActive && 'font-medium',
                    )}
                  >
                    {item.label}
                  </span>
                </>
              )
              if (item.to) {
                return (
                  <a
                    key={item.value}
                    data-slot="bottom-navigation-item"
                    data-active={isActive ? '' : undefined}
                    aria-current={isActive ? 'page' : undefined}
                    href={item.to}
                    className={itemClass}
                    onClick={(e) => {
                      // Let the browser handle the navigation; still notify state.
                      handleSelect(item)
                      // Consumers using a router can call preventDefault in onSelect.
                      if (e.defaultPrevented) return
                    }}
                  >
                    {inner}
                  </a>
                )
              }
              return (
                <button
                  key={item.value}
                  data-slot="bottom-navigation-item"
                  data-active={isActive ? '' : undefined}
                  aria-current={isActive ? 'page' : undefined}
                  type="button"
                  className={itemClass}
                  onClick={() => handleSelect(item)}
                >
                  {inner}
                </button>
              )
            })}
          </nav>
        )
      },
    )
    BottomNavigation.displayName = 'BottomNavigation'
    
    export { BottomNavigation }
  • components/ui/bottom-navigation/index.ts 0.1 kB
    export { BottomNavigation, type BottomNavigationProps, type BottomNavItem } from './BottomNavigation'

Raw manifest: https://uipkge.dev/r/react/bottom-navigation.json