Bottom Navigation

A bottom bar whose highlight glides to the selected item, and that can fold to it or slide away while the page scrolls down.

Today

  • SSpotify08:12€10.99
  • FFigma09:30€15.00
  • UUber12:42€6.05
  • NNotion14:05€8.00
  • AAirbnb17:03€256.73
  • VVercel18:21€20.00
  • AApple19:18€2.99
  • DDribbble20:47€52.92
  • NNetflix21:15€13.49
  • LLinear22:02€8.00
"use client"

import * as React from "react"
import {
  BottomNavigation,
  BottomNavigationHighlight,
  BottomNavigationHighlightItem,
  BottomNavigationAccessory,
  BottomNavigationItem,
  BottomNavigationLabel,
} from "@animate-ui/react/primitives/bottom-navigation"
import { Button } from "@animate-ui/react/primitives/buttons/button"

import { House } from "@/components/animate-ui/icons/house"
import { Plus } from "@/components/animate-ui/icons/plus"
import { Search } from "@/components/animate-ui/icons/search"
import { User } from "@/components/animate-ui/icons/user"
import { Wallet } from "@/components/animate-ui/icons/wallet"

const ITEMS = [
  { value: "home", label: "Home", Icon: House },
  { value: "search", label: "Search", Icon: Search },
  { value: "wallet", label: "Wallet", Icon: Wallet },
  { value: "profile", label: "Profile", Icon: User },
]

const PAYMENTS = [
  { name: "Spotify", time: "08:12", amount: "€10.99" },
  { name: "Figma", time: "09:30", amount: "€15.00" },
  { name: "Uber", time: "12:42", amount: "€6.05" },
  { name: "Notion", time: "14:05", amount: "€8.00" },
  { name: "Airbnb", time: "17:03", amount: "€256.73" },
  { name: "Vercel", time: "18:21", amount: "€20.00" },
  { name: "Apple", time: "19:18", amount: "€2.99" },
  { name: "Dribbble", time: "20:47", amount: "€52.92" },
  { name: "Netflix", time: "21:15", amount: "€13.49" },
  { name: "Linear", time: "22:02", amount: "€8.00" },
]

export default function BottomNavigationDemo({
  scrollBehavior = "minimize",
  labels = true,
  accessory = true,
}: {
  scrollBehavior?: "none" | "hide" | "minimize"
  labels?: boolean | string
  accessory?: boolean | string
}) {
  const showLabels = String(labels) === "true"
  const showAccessory = String(accessory) === "true"
  const [value, setValue] = React.useState("home")
  const [presses, setPresses] = React.useState(0)
  const scrollRef = React.useRef<HTMLDivElement>(null)

  return (
    <div className="relative h-[440px] w-[340px] overflow-hidden rounded-[2rem] border bg-background shadow-sm">
      <div ref={scrollRef} className="h-full overflow-y-auto px-5 pt-6 pb-24">
        <p className="mb-3 text-xs font-medium tracking-wide text-muted-foreground uppercase">
          Today
        </p>
        <ul className="flex flex-col gap-4">
          {PAYMENTS.map((payment) => (
            <li key={payment.name} className="flex items-center gap-3">
              <span className="flex size-9 items-center justify-center rounded-full bg-muted text-sm font-medium">
                {payment.name[0]}
              </span>
              <span className="flex flex-1 flex-col">
                <span className="text-sm font-medium">{payment.name}</span>
                <span className="text-xs text-muted-foreground">
                  {payment.time}
                </span>
              </span>
              <span className="text-sm font-medium tabular-nums">
                {payment.amount}
              </span>
            </li>
          ))}
        </ul>
      </div>
      <div className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-linear-to-t from-background to-transparent" />
      <div className="pointer-events-none absolute inset-x-0 bottom-4 flex justify-center">
        <BottomNavigation
          value={value}
          onValueChange={setValue}
          scrollBehavior={scrollBehavior}
          scrollContainer={scrollRef}
          aria-label="Main"
          className="pointer-events-auto flex items-center gap-2"
        >
          <BottomNavigationHighlight className="absolute inset-0 z-0 rounded-full bg-muted">
            <ul className="flex items-center rounded-full border bg-background/80 p-1.5 shadow-lg backdrop-blur">
              {ITEMS.map(({ value: item, label, Icon }) => (
                <li key={item}>
                  <BottomNavigationHighlightItem value={item}>
                    <BottomNavigationItem value={item} asChild>
                      <button
                        type="button"
                        aria-label={label}
                        className="relative z-10 flex h-11 items-center justify-center rounded-full px-3.5 text-muted-foreground outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 hover:text-foreground data-[state=active]:text-foreground"
                      >
                        <Icon animate={value === item} initial={false} size={20} />
                        {showLabels && (
                          <BottomNavigationLabel className="pl-2 text-sm font-medium">
                            {label}
                          </BottomNavigationLabel>
                        )}
                      </button>
                    </BottomNavigationItem>
                  </BottomNavigationHighlightItem>
                </li>
              ))}
            </ul>
          </BottomNavigationHighlight>
          {showAccessory && (
            <BottomNavigationAccessory className="rounded-full border bg-background/80 p-1.5 shadow-lg backdrop-blur">
              <Button
                type="button"
                aria-label="New payment"
                onClick={() => setPresses((count) => count + 1)}
                className="flex size-11 items-center justify-center rounded-full text-muted-foreground outline-none transition-colors duration-300 ease-out hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
              >
                <Plus key={presses} animate={presses > 0} size={20} />
              </Button>
            </BottomNavigationAccessory>
          )}
        </BottomNavigation>
      </div>
    </div>
  )
}

Installation

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Usage

import {
  BottomNavigation,
  BottomNavigationAccessory,
  BottomNavigationHighlight,
  BottomNavigationHighlightItem,
  BottomNavigationItem,
  BottomNavigationLabel,
} from '@animate-ui/react/primitives/bottom-navigation';

const ITEMS = [
  { value: 'home', href: '/', label: 'Home', icon: <HomeIcon /> },
  { value: 'search', href: '/search', label: 'Search', icon: <SearchIcon /> },
];

export function AppNavigation() {
  return (
    <BottomNavigation
      defaultValue="home"
      scrollBehavior="minimize"
      aria-label="Main"
      className="fixed inset-x-0 bottom-4 mx-auto flex w-fit items-center gap-2"
    >
      <BottomNavigationHighlight className="absolute inset-0 rounded-full bg-muted">
        <ul className="flex rounded-full border bg-background p-1.5">
          {ITEMS.map((item) => (
            <li key={item.value}>
              <BottomNavigationHighlightItem value={item.value}>
                <BottomNavigationItem
                  value={item.value}
                  href={item.href}
                  className="flex items-center px-4 py-2"
                >
                  {item.icon}
                  <BottomNavigationLabel className="pl-2">
                    {item.label}
                  </BottomNavigationLabel>
                </BottomNavigationItem>
              </BottomNavigationHighlightItem>
            </li>
          ))}
        </ul>
      </BottomNavigationHighlight>
      <BottomNavigationAccessory className="rounded-full border bg-background p-1.5">
        <button className="size-11 rounded-full hover:bg-muted">+</button>
      </BottomNavigationAccessory>
    </BottomNavigation>
  );
}

Items are links: give them an href, or asChild with your router's link or a button. The selected item gets aria-current="page" and data-state="active". With a router, pass the current route as value so the highlight follows navigation. BottomNavigationHighlight and BottomNavigationHighlightItem are optional; without them, style the active item with data-state="active".

scrollBehavior sets what the bar does while the page scrolls down: "minimize" folds it to the selected item, like iOS' tab bar, "hide" slides it out, and "none" (the default) leaves it. It comes back as soon as the page scrolls up or reaches the top, an item takes focus, or the bar is tapped. It listens to the window, or to scrollContainer when the page scrolls inside an element. To fold, items shrink to nothing, padding included: space them with padding rather than a gap, which would stay.

BottomNavigationLabel and BottomNavigationAccessory are optional:

  • A label shows only on the selected item, so the bar stays icons and one name. Its text stays in the accessibility tree, so it names the item even while closed.
  • The accessory (a button, a player, a search field) stays beside the bar, folded or not: it follows the bar as it folds. Pressing or focusing it leaves the bar folded.

A closed label takes no room: put its spacing inside it (pl-2), not on the item.

Animation

  • The highlight slides to the selected item on a spring (stiffness 300, damping 30): one surface is handed from the previous item to the new one, at full opacity (Highlight).
  • Items scale to 0.97 while pressed, on the buttons' spring.
  • Labels open on the highlight's spring, so the highlight widens with the selected item: the new label grows from nothing out of a 4px blur as the previous one narrows and blurs away.
  • Both scroll behaviors start once the page has scrolled down by 6px and run on a critically damped spring (stiffness 300, damping 35).
    • "minimize": the other items narrow to nothing as they fade out into a 4px blur, the bar closing around the selected one with its highlight; they come back the same way.
    • "hide": the bar slides out past the bottom edge, and back on the way up.
  • <AnimateUIProvider tempo> paces every spring.
  • Under reduced motion, the highlight moves at once, items don't scale, labels change width at once (fading), the bar folds at once, and the hidden bar fades out instead of sliding (and can’t be clicked while hidden).

API Reference

BottomNavigation

PropTypeDefault
defaultValue?
string-
value?
string-
onValueChange?
(value: string) => void-
scrollBehavior?
'none' | 'hide' | 'minimize''none'
scrollContainer?
React.RefObject<HTMLElement | null>-
transition?
Transition{ type: 'spring', stiffness: 300, damping: 35, restDelta: 0.01 }
...props?
HTMLMotionProps<'nav'>-

BottomNavigationHighlight

Takes the Highlight props, except controlledItems and value.

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 300, damping: 30 }

BottomNavigationHighlightItem

Takes the HighlightItem props.

PropTypeDefault
value
string-

BottomNavigationItem

PropTypeDefault
asChild?
booleanfalse
value
string-
tapScale?
number0.97
...props?
HTMLMotionProps<'a'>-

BottomNavigationLabel

Goes inside a BottomNavigationItem.

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 300, damping: 30 }
...props?
HTMLMotionProps<'span'>-

BottomNavigationAccessory

PropTypeDefault
...props?
HTMLMotionProps<'div'>-

useBottomNavigation

Returns activeValue, handleValueChange, scrollBehavior and minimized.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/25/2026