Components
BottomNav
The tab bar fixed to the bottom of the screen below md, with 3 to 5 top-level destinations. It is separate from Sidebar: no shared item list; an app that has both renders its own items in each. The bar adds spacing-safe-bottom to its own bottom padding, which reads the safe area and falls back to 0px. The page viewport needs viewport-fit=cover.
Preview
import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconInbox, IconSearch } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"
const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }
<div className="w-80 max-w-full overflow-hidden rounded-container border border-border bg-background-surface">
<div className="flex flex-col gap-xs p-lg">
<p className="text-sm font-medium">Your workspace</p>
<p className="text-xs text-text-secondary">Choose a destination below.</p>
</div>
<BottomNav label="Main" value={destination} className="relative md:block">
<BottomNavItem href="#home" label="Home" icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} onClick={selectDestination} />
<BottomNavItem href="#search" label="Search" icon={<IconSearch size={20} />} onClick={selectDestination} />
<BottomNavItem href="#inbox" label="Inbox" icon={<IconInbox size={20} />} count={3} onClick={selectDestination} />
</BottomNav>
</div>Usage
When to use
- Show a short list of top-level destinations at the bottom of a small screen.
When not to use
- Put actions such as Create or Share in the navigation. Use Button.
- Show the same bar next to a visible Sidebar.
Examples
Three destinations
import { IconHome, IconHomeFilled, IconInbox, IconSearch } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"
<BottomNav label="Main" className="relative md:block">
<BottomNavItem href="#home" icon={<IconHome size={20} />} label="Home" activeIcon={<IconHomeFilled size={20} />} active />
<BottomNavItem href="#search" icon={<IconSearch size={20} />} label="Search" />
<BottomNavItem href="#inbox" icon={<IconInbox size={20} />} label="Inbox" count={3} />
</BottomNav>Five destinations
import { IconHome, IconHomeFilled, IconInbox, IconLayoutGrid, IconSearch, IconUser } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"
<BottomNav label="Main" className="relative md:block">
<BottomNavItem href="#home" icon={<IconHome size={20} />} label="Home" activeIcon={<IconHomeFilled size={20} />} active />
<BottomNavItem href="#search" icon={<IconSearch size={20} />} label="Search" />
<BottomNavItem href="#inbox" icon={<IconInbox size={20} />} label="Inbox" count={3} />
<BottomNavItem href="#projects" icon={<IconLayoutGrid size={20} />} label="Projects" />
<BottomNavItem href="#profile" icon={<IconUser size={20} />} label="Profile" />
</BottomNav>Current route
import { IconHome, IconHomeFilled, IconInbox, IconLayoutGrid, IconSearch } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"
<BottomNav label="Main" value="#inbox" className="relative md:block">
<BottomNavItem href="#home" icon={<IconHome size={20} />} label="Home" activeIcon={<IconHomeFilled size={20} />} />
<BottomNavItem href="#search" icon={<IconSearch size={20} />} label="Search" />
<BottomNavItem href="#inbox" icon={<IconInbox size={20} />} label="Inbox" count={3} />
<BottomNavItem href="#projects" icon={<IconLayoutGrid size={20} />} label="Projects" />
</BottomNav>Custom link elements
import { IconHome, IconHomeFilled, IconInbox, IconSearch } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"
<BottomNav label="Main" className="relative md:block">
<BottomNavItem href="#home" icon={<IconHome size={20} />} label="Home" activeIcon={<IconHomeFilled size={20} />} active asChild>
<a href="#home">Home</a>
</BottomNavItem>
<BottomNavItem href="#search" icon={<IconSearch size={20} />} label="Search" asChild>
<a href="#search">Search</a>
</BottomNavItem>
<BottomNavItem href="#inbox" icon={<IconInbox size={20} />} label="Inbox" asChild>
<a href="#inbox">Inbox</a>
</BottomNavItem>
</BottomNav>Do and don't
Do
- Use 3 to 5 destinations with short labels.
- Pad page content so the bar never covers the last row.
- In these embedded demos, relative md:block keeps the bar inside its cell and visible on desktop. Omit those demo classes for a fixed mobile bar in an application.
- Use the current route as value, or set active on one item. The component does not own navigation state.
Don't
- Do not put actions (Create, Share) in the bar; use a Button.
- Do not show it next to an open Sidebar.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| label | string | - | Main | On BottomNav: accessible name of the navigation landmark. On BottomNavItem: visible destination text and accessible name. |
| countLabel | (label: string, count: number) => string | - | - | Names an item that shows a count, replacing its accessible name: (label, count) => `{label}, {count} new` by default. Vietnamese: `{label}, {count} mục mới`. Optional. |
| value | string | - | - | On BottomNav: active destination href, usually supplied by the application router. Takes precedence over item active props. BottomNav does not change it on click. |
| href required | string | - | - | On BottomNavItem: destination URL and selection id. Required even with asChild. |
| icon required | node | - | - | On BottomNavItem: the regular 20px icon. |
| activeIcon | node | - | - | On BottomNavItem: optional icon for the active state, such as a Filled Tabler icon. |
| active | boolean | - | false | On BottomNavItem: marks this destination current when BottomNav has no value. Selection still uses normal link navigation. |
| count | number | - | - | On BottomNavItem: a count above zero adds a badge and names the count through countLabel. Zero and negative counts are hidden. |
| asChild | boolean | - | false | On BottomNavItem: render the destination link behavior on its only child. |
| onClick | React.MouseEventHandler<HTMLAnchorElement> | - | - | On BottomNavItem: native link click handler. Use application navigation or preventDefault() for an in-place demonstration. |
Slots
| Name | Required | Description |
|---|---|---|
| children | yes | 3 to 5 BottomNavItem with href, icon, label, optional activeIcon, count and active. |
Anatomy
| Part | Description |
|---|---|
| root | Fixed bar on the material-thin surface, hidden from md up, with a safe-area inset. |
| item | One destination link: 20px icon over a font-size-xs label, at least size-target-min tall. |
| icon | The 20px icon, with an optional activeIcon for its selected appearance. |
| label | The item text. |
| badge | Optional count Badge on the icon. |
Accessibility
Role: navigation
| Keys | Action |
|---|---|
| Tab | Moves between destinations (links), in order. |
| Enter | Follows the link. |
- A nav landmark named by label. Items are links, not tabs: no role tab and no arrow-key roving.
- The active item has aria-current="page" and uses the Filled icon (DESIGN 9.1).
- Every item shows a text label; icon-only bars are not allowed.
- A count Badge adds its number to the accessible name, through countLabel.
Install
npx werewolf-ui add bottom-navRun npx werewolf-ui init once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See Installation for requirements and the alternative registry setup.