Fixed bottom tab bar for phones, with a gradient that fades the page into it and an active item you hand it from your router
Open the full-page preview to try the bar against real routes, outside the frame below.
import { useState } from "react";import { useIcon } from "@/lib/icon-context";import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";import { PhoneFrame } from "@/lib/docs/phone-frame";const LABELS: Record<string, string> = {home: "Home",search: "Search",likes: "Likes",profile: "Profile",};export default function BottomNavDemo() {const [value, setValue] = useState("home");const HomeIcon = useIcon("home");const SearchIcon = useIcon("search");const HeartIcon = useIcon("heart");const UserIcon = useIcon("user");return (<PhoneFrame route={value}><p className="px-5 text-lg font-semibold">{LABELS[value]}</p>{Array.from({ length: 10 }).map((_, i) => (<div key={i} aria-hidden className="mx-5 mt-3 h-16 rounded-xl bg-muted" />))}<BottomNav value={value} onValueChange={setValue}><BottomNavItem value="home" icon={<HomeIcon />}>Home</BottomNavItem><BottomNavItem value="search" icon={<SearchIcon />}>Search</BottomNavItem><BottomNavItem value="likes" icon={<HeartIcon />}>Likes</BottomNavItem><BottomNavItem value="profile" icon={<UserIcon />}>Profile</BottomNavItem></BottomNav>
The frame above drives the bar with local state. To try it against real routes, open the full-page preview on your phone: three pages, one bar, the router deciding what is active.
Installation
npx shadcn@latest add https://www.deltacomponents.dev/r/bottom-nav.json
Usage
1import Link from "next/link";2import { usePathname } from "next/navigation";3import { Home, Search, User } from "lucide-react";4import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";56const pathname = usePathname();78<BottomNav value={pathname} linkComponent={Link} className="md:hidden">9 <BottomNavItem value="/" href="/" icon={<Home />}>Home</BottomNavItem>10 <BottomNavItem value="/search" href="/search" icon={<Search />}>Search</BottomNavItem>11 <BottomNavItem value="/profile" href="/profile" icon={<User />}>Profile</BottomNavItem>12</BottomNav>
The nav never reads the URL itself: value is whatever your router says is current. linkComponent is how a Next app keeps client-side routing (omit it and items are plain <a> tags). The component carries no breakpoint, so md:hidden is the layout's call. Re-tapping the active item scrolls back to the top.
Basic
import { useState } from "react";import { useIcon } from "@/lib/icon-context";import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";import { PhoneFrame } from "@/lib/docs/phone-frame";const LABELS: Record<string, string> = {home: "Home",search: "Search",likes: "Likes",profile: "Profile",};export default function BottomNavDemo() {const [value, setValue] = useState("home");const HomeIcon = useIcon("home");const SearchIcon = useIcon("search");const HeartIcon = useIcon("heart");const UserIcon = useIcon("user");return (<PhoneFrame route={value}><p className="px-5 text-lg font-semibold">{LABELS[value]}</p>{Array.from({ length: 10 }).map((_, i) => (<div key={i} aria-hidden className="mx-5 mt-3 h-16 rounded-xl bg-muted" />))}<BottomNav value={value} onValueChange={setValue}><BottomNavItem value="home" icon={<HomeIcon />}>Home</BottomNavItem><BottomNavItem value="search" icon={<SearchIcon />}>Search</BottomNavItem><BottomNavItem value="likes" icon={<HeartIcon />}>Likes</BottomNavItem><BottomNavItem value="profile" icon={<UserIcon />}>Profile</BottomNavItem></BottomNav>
Fade
The page dissolves into the background over the 160px above the bar. fade
says where that lands: at the default 0.5 it is solid from the icons' top edge
down; lower lets it run into the icons, 1 ends it at the bar's top edge.
Scroll the screen a little and drag the slider.
import { useState } from "react";import { useIcon } from "@/lib/icon-context";import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";import { PhoneFrame } from "@/lib/docs/phone-frame";import { Slider } from "@/app/components/ui/slider";const LINES = Array.from({ length: 24 },(_, i) => `Line ${i + 1} of the page, scrolling up under the bar.`);export default function BottomNavFade() {const [value, setValue] = useState("home");const [fade, setFade] = useState(0.5);const HomeIcon = useIcon("home");const SearchIcon = useIcon("search");const HeartIcon = useIcon("heart");const UserIcon = useIcon("user");return (<div className="flex flex-col items-center gap-5"><div className="flex w-56 items-center gap-3 text-sm text-muted-foreground">fade<Slideraria-label="fade"min={0}max={1}step={0.05}value={fade}onValueChange={(v) => setFade(v as number)}/><span className="w-8 tabular-nums text-foreground">{fade.toFixed(2)}</span></div><PhoneFrame route={value}><div className="px-5 pb-28">{LINES.map((line, i) => (<p key={i} className="mt-3 text-sm">{line}</p>
Icon Only
Icon-only keeps each label as screen-reader text.
import { useState } from "react";import { useIcon } from "@/lib/icon-context";import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";import { PhoneFrame } from "@/lib/docs/phone-frame";const LABELS: Record<string, string> = {home: "Home",search: "Search",likes: "Likes",profile: "Profile",};export default function BottomNavIconOnly() {const [value, setValue] = useState("home");const HomeIcon = useIcon("home");const SearchIcon = useIcon("search");const HeartIcon = useIcon("heart");const UserIcon = useIcon("user");return (<PhoneFrame route={value}><p className="px-5 text-lg font-semibold">{LABELS[value]}</p>{Array.from({ length: 10 }).map((_, i) => (<div key={i} aria-hidden className="mx-5 mt-3 h-16 rounded-xl bg-muted" />))}<BottomNav value={value} onValueChange={setValue} labels={false}><BottomNavItem value="home" icon={<HomeIcon />}>Home</BottomNavItem><BottomNavItem value="search" icon={<SearchIcon />}>Search</BottomNavItem><BottomNavItem value="likes" icon={<HeartIcon />}>Likes</BottomNavItem><BottomNavItem value="profile" icon={<UserIcon />}>Profile</BottomNavItem></BottomNav>
Three Items
Four or more items fill the bar edge to edge, as iPhone's tab bar does. Two or three are drawn in toward the middle (three take 72% of the bar, two 56%), so they don't sit a thumb's width apart.
import { useState } from "react";import { useIcon } from "@/lib/icon-context";import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";import { PhoneFrame } from "@/lib/docs/phone-frame";const LABELS: Record<string, string> = {home: "Home",search: "Search",profile: "Profile",};export default function BottomNavThreeItems() {const [value, setValue] = useState("home");const HomeIcon = useIcon("home");const SearchIcon = useIcon("search");const UserIcon = useIcon("user");return (<PhoneFrame route={value}><p className="px-5 text-lg font-semibold">{LABELS[value]}</p>{Array.from({ length: 10 }).map((_, i) => (<div key={i} aria-hidden className="mx-5 mt-3 h-16 rounded-xl bg-muted" />))}<BottomNav value={value} onValueChange={setValue}><BottomNavItem value="home" icon={<HomeIcon />}>Home</BottomNavItem><BottomNavItem value="search" icon={<SearchIcon />}>Search</BottomNavItem><BottomNavItem value="profile" icon={<UserIcon />}>Profile</BottomNavItem></BottomNav></PhoneFrame>);}
Solid
A blurred solid strip instead of the gradient, for pages whose content shouldn't bleed into the bar.
import { useState } from "react";import { useIcon } from "@/lib/icon-context";import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";import { PhoneFrame } from "@/lib/docs/phone-frame";const LABELS: Record<string, string> = {home: "Home",search: "Search",likes: "Likes",profile: "Profile",};export default function BottomNavSolid() {const [value, setValue] = useState("home");const HomeIcon = useIcon("home");const SearchIcon = useIcon("search");const HeartIcon = useIcon("heart");const UserIcon = useIcon("user");return (<PhoneFrame route={value}><p className="px-5 text-lg font-semibold">{LABELS[value]}</p>{Array.from({ length: 10 }).map((_, i) => (<div key={i} aria-hidden className="mx-5 mt-3 h-16 rounded-xl bg-muted" />))}<BottomNav value={value} onValueChange={setValue} gradient={false}><BottomNavItem value="home" icon={<HomeIcon />}>Home</BottomNavItem><BottomNavItem value="search" icon={<SearchIcon />}>Search</BottomNavItem><BottomNavItem value="likes" icon={<HeartIcon />}>Likes</BottomNavItem><BottomNavItem value="profile" icon={<UserIcon />}>Profile</BottomNavItem></BottomNav>