Skip to content
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

Your workspace

Choose a destination below.

Props

  • count
  • destinations
  • badges
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>
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

NameTypeValuesDefaultDescription
labelstring-MainOn 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.
valuestring--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 requiredstring--On BottomNavItem: destination URL and selection id. Required even with asChild.
icon requirednode--On BottomNavItem: the regular 20px icon.
activeIconnode--On BottomNavItem: optional icon for the active state, such as a Filled Tabler icon.
activeboolean-falseOn BottomNavItem: marks this destination current when BottomNav has no value. Selection still uses normal link navigation.
countnumber--On BottomNavItem: a count above zero adds a badge and names the count through countLabel. Zero and negative counts are hidden.
asChildboolean-falseOn BottomNavItem: render the destination link behavior on its only child.
onClickReact.MouseEventHandler<HTMLAnchorElement>--On BottomNavItem: native link click handler. Use application navigation or preventDefault() for an in-place demonstration.

Slots

NameRequiredDescription
childrenyes3 to 5 BottomNavItem with href, icon, label, optional activeIcon, count and active.

Anatomy

PartDescription
rootFixed bar on the material-thin surface, hidden from md up, with a safe-area inset.
itemOne destination link: 20px icon over a font-size-xs label, at least size-target-min tall.
iconThe 20px icon, with an optional activeIcon for its selected appearance.
labelThe item text.
badgeOptional count Badge on the icon.

Accessibility

Role: navigation

KeysAction
TabMoves between destinations (links), in order.
EnterFollows 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-nav

Run 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.