Bottom Navigation
bottom-navigation ui 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
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/bottom-navigation.json $ npx shadcn@latest add https://uipkge.dev/r/react/bottom-navigation.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/bottom-navigation.json $ bunx shadcn@latest add https://uipkge.dev/r/react/bottom-navigation.json 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