Bottom Nav
zyeon/bottom-navFreeComponent
A mobile tab bar with badges, safe-area padding and an indicator that measures the active item and glides to it.
Install it
npx shadcn@latest add https://ui.zyeon.ai/r/bottom-nav.jsonSource
1"use client"23import * as React from "react"4import { cn } from "@/lib/utils"56export interface BottomNavItem {7 key: string8 label: string9 icon: React.ReactNode10 /** Selected-state glyph (usually the filled twin of `icon`); falls back to `icon`. */11 activeIcon?: React.ReactNode12 /** Unread count. 0 / undefined renders nothing; anything above 99 renders "99+". */13 badge?: number14 /** When set the item renders a real <a> — routing stays with the consumer. */15 href?: string16 /** Per-item side effect, fired alongside onValueChange. */17 onSelect?: () => void18}1920export interface BottomNavProps extends React.HTMLAttributes<HTMLElement> {21 items: BottomNavItem[]22 /** Controlled: key of the active item. */23 value: string24 onValueChange?: (key: string) => void25 /** Text labels for every item, only under the active one, or never (icons keep their aria-label). */26 showLabels?: "always" | "active" | "never"27 indicator?: "bar" | "pill" | "none"28 /** Accessible name of the <nav> landmark. */29 label?: string30}3132interface Metrics {33 left: number34 width: number35}3637/**38 * Mobile tab bar. The indicator is measured from the active item's own box and39 * moved with a transform, so it tracks any item count / width without the40 * consumer declaring geometry.41 */42export const BottomNav = React.forwardRef<HTMLElement, BottomNavProps>(43 (44 {45 items,46 value,47 onValueChange,48 showLabels = "always",49 indicator = "bar",50 label = "Primary",51 className,52 ...props53 },54 ref,55 ) => {56 const rowRef = React.useRef<HTMLDivElement | null>(null)57 const itemRefs = React.useRef(new Map<string, HTMLElement>())58 const [metrics, setMetrics] = React.useState<Metrics | null>(null)59 // no transition on the first frame: place the indicator, then allow gliding from the next60 // one (otherwise it flies in from 0 at mount)61 const [glide, setGlide] = React.useState(false)6263 const setItemRef = (key: string) => (node: HTMLElement | null) => {64 if (node) itemRefs.current.set(key, node)65 else itemRefs.current.delete(key)66 }6768 // an inline items literal is a new array every render; joined into a key, unchanged contents69 // mean the ResizeObserver is not rebuilt.70 const itemsKey = items.map(item => item.key).join("\n")7172 React.useEffect(() => {73 if (indicator === "none") return74 const row = rowRef.current75 const active = itemRefs.current.get(value)76 if (!row || !active) {77// … truncated
What it pulls in
Other registry items
Files it writes
More from zyeon
All 893 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| A/B Test Resultab-test-result | zyeon | Components | Paid | 2 deps · 2 files | |
| Accordionaccordion | zyeon | Components | Free | 1 dep | |
| Activity Feedactivity-feed | zyeon | Components | Free | 2 deps · 2 files | |
| Activity Heatmapactivity-heatmap | zyeon | Components | Paid | 1 dep · 2 files | |
| Address Formaddress-form | zyeon | Components | Paid | 1 dep | |
| Agent Graphagent-graph | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Handoffagent-handoff | zyeon | Components | Free | 1 dep | |
| Agent Inboxagent-inbox | zyeon | Components | Free | 2 deps · 2 files |
