# React Bottom Navigation Bar in Next.js: Safe Areas, Badges

> Build a React bottom navigation bar in Next.js: 3 to 5 link tabs, safe areas, badges screen readers announce, hide on scroll, and when a sidebar fits better.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 9, 2026
- Category: [Mobile UI in React](https://wingo-ui.com/blog/category/mobile-ui)
- Reading time: 8 min
- Canonical: https://wingo-ui.com/blog/react-bottom-navigation-bar

## TL;DR

A React bottom navigation bar is a fixed nav of 3 to 5 links at the bottom of the screen, with aria-current="page" on the active link, padding for env(safe-area-inset-bottom) under viewport-fit=cover, and bottom padding on the page equal to the bar's height so the last row stays visible. Keep the labels visible, put badge counts in each link's accessible name, and only hide the bar on scroll if it follows the finger and comes back on scroll up. Use it below 768px with a sidebar above that, and with more than five destinations keep the four most used in the bar and the rest in a drawer.

A React bottom navigation bar is the tab bar that native apps have trained everyone to expect: three to five destinations on the bottom edge, where the thumb already is. On the web it takes more than a fixed `<div>` of icons. The bar has to clear the iPhone home indicator, keep the last row of the page visible, tell a screen reader that Inbox has 4 unread messages, follow the router, and decide what a big center button and a long scroll should do. This guide covers each with the Wingo UI [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar), then the cases where a [Sidebar](https://wingo-ui.com/components/sidebar) is the better choice.

## When does a web app need a bottom navigation bar?

When people use it on phones and move between three to five top-level sections many times a session. Android's [navigation bar guidance](https://developer.android.com/develop/ui/compose/components/navigation-bar) lists the conditions: "Three to five destinations of equal importance", "Compact window sizes" and "Consistent destinations across app screens". A CRM with Home, Clients, Inbox and Profile fits. A marketing site, a docs site or a checkout does not, because nobody hops between their sections.

The bottom edge is the reason to bother. In Steven Hoober's [2013 field study](https://www.uxmatters.com/mt/archives/2013/02/how-do-users-really-hold-mobile-devices.php) (1,333 observations of people using mobile devices), 49% of those touching the screen held the phone in one hand. A hamburger menu in a top corner sits far from that thumb and hides every destination behind an extra tap. In a bottom tab bar web app, every destination stays visible and one tap away, and the active tab tells people where they are.

## What does a bottom tab bar need to work in a browser?

Five jobs that a native tab bar gets from the platform and a web page has to do itself:

| Concern | What to do | In the Bottom Tab Bar |
| --- | --- | --- |
| Semantics | A `<nav>` of links, `aria-current="page"` on the active one | `aria-label="Primary"`; tabs without `href` render as buttons |
| Routing | The active tab follows the URL, nested routes included | `currentPath` picks the longest matching `href`; `linkComponent` takes `next/link` |
| Home indicator | `viewport-fit=cover`, then pad the inside with `env(safe-area-inset-bottom)` | `safeArea`, on by default |
| Covered content | Pad the page by the bar's real height | Writes `--bottom-bar-offset` on `<html>` |
| Touch | Targets of 44px or more | Each tab fills its slot, 56 or 64px tall |

Build the tabs from links. The [ARIA tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) describes "layered sections of content, known as tab panels, that display one panel of content at a time". A tab bar loads another page, so `role="tablist"` would announce a widget that is not there.

Here is a React mobile bottom navigation for the Next.js App Router, in two files:

```tsx
// app/(app)/mobile-nav.tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { House, Inbox, Plus, UserRound, Users } from "lucide-react";
import { BottomTabBar, type BottomTabBarItem } from "@/components/ui/bottom-tab-bar";

const tabs: BottomTabBarItem[] = [
  { label: "Home", href: "/", icon: <House />, activeIcon: <House className="fill-current/20" /> },
  { label: "Clients", href: "/clients", icon: <Users />, activeIcon: <Users className="fill-current/20" /> },
  { label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
  { label: "Profile", href: "/profile", icon: <UserRound />, dot: true },
];

export function MobileNav() {
  return (
    <BottomTabBar
      className="md:hidden"
      items={tabs}
      currentPath={usePathname()}
      linkComponent={Link}
      action={{ label: "New client", icon: <Plus />, href: "/clients/new" }}
    />
  );
}
```

```tsx
// app/(app)/layout.tsx
import type { ReactNode } from "react";
import type { Viewport } from "next";
import { MobileNav } from "./mobile-nav";

// without cover, iOS keeps the page inside the safe area and env() reports 0
export const viewport: Viewport = { width: "device-width", initialScale: 1, viewportFit: "cover" };

export default function AppLayout({ children }: { children: ReactNode }) {
  return (
    <>
      <main className="pb-(--bottom-bar-offset)">{children}</main>
      <MobileNav />
    </>
  );
}
```

The bar is fixed to the viewport and measures itself, safe area included, so the padding on `<main>` is always exact; hidden by `md:hidden` on a desktop, it measures 0. A guessed `pb-16` is wrong as soon as the inset or the size changes. The [safe area guide](https://wingo-ui.com/blog/tailwind-safe-area-insets) explains the CSS underneath. Render the bar in the server HTML and hide it with CSS: deciding with a `useIsMobile()` hook renders nothing on the server, and the bar pops in after hydration, as the [useMobile hook guide](https://wingo-ui.com/blog/react-use-mobile-hook-ssr) shows.

> Live demo (Bottom Tab Bar): Tap Clients, open a client, then tap Clients again: the tab goes back to its list, and one more tap scrolls to the top. The raised Add button opens a sheet instead of switching tabs. Try it at [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) and install it with `npx wingo-ui@latest add bottom-tab-bar`.

## How should tab bar badges and labels behave?

A count for things that need action, a dot for something new, both read as part of the tab's name. `badge: 4` renders a count (99+ above 99) and `dot: true` an 8px dot, red by default (`badgeTone="danger"`) because people read a red count as unread. The visible badge is `aria-hidden`, and the link is named "Inbox, 4 unread" (`badgeLabel`) or "Profile, new" (`dotLabel`), so a screen reader never reads a stray number. Apple's [tab bar guidelines](https://developer.apple.com/design/human-interface-guidelines/tab-bars) (updated June 2026) say "Reserve badges for critical information so you don't dilute their impact and meaning": unread messages and pending approvals, not a new feature.

Keep `showLabels="always"`. The same guidelines say "Include tab labels to help with navigation" and "Use single words whenever possible". `"active"` and `"never"` exist for icon-heavy consumer apps; with `"never"`, labels stay in the DOM for screen readers and become `title` tooltips. `activeIcon` swaps in a second glyph while the tab is active, such as a filled version of the outline icon. Apple goes further and asks you to "Prefer filled symbols or icons for consistency with the platform" on every tab.

## Should the center button be a tab, an action or a FAB?

An action, never a tab, and only when one creation action dominates the app. Apple is strict here: "Use a tab bar to support navigation, not to provide actions." We still ship the raised center button, because in an app built around one verb (Post, Sell, Scan) that verb has earned the most reachable spot. The `action` prop renders it in the middle (with four tabs: two, the action, two), filled with the `tone` and named by its `label`. It never takes the active pill, so nobody mistakes it for where they are.

When the app has several things to create, or you follow Apple's rule, leave the bar to navigation and use a [FAB](https://wingo-ui.com/components/fab). It reads `--bottom-bar-offset`, so it floats 16px above the tab bar and the home indicator without counting the inset twice:

```tsx
import { FileText, UserPlus } from "lucide-react";
import { Fab, type FabAction } from "@/components/ui/fab";

const actions: FabAction[] = [
  { id: "client", label: "New client", icon: <UserPlus />, href: "/clients/new" },
  { id: "invoice", label: "New invoice", icon: <FileText />, href: "/invoices/new" },
];

export function CreateFab() {
  return <Fab className="md:hidden" label="Add" actions={actions} />;
}
```

> Live demo (FAB): Tap Add: the page dims and the create actions rise from the button. Scroll the list and the label folds down to the plus. Try it at [FAB](https://wingo-ui.com/components/fab) and install it with `npx wingo-ui@latest add fab`.

## Should a bottom navigation bar hide on scroll?

Only on long feeds and reading screens, and only if it comes back the moment the user might need it. Apple warns that "If you hide the tab bar, people can forget which area of the app they're in". Its iOS guidance describes minimizing a tab bar that carries an accessory, such as the MiniPlayer in Music, while people scroll down; tapping a tab or scrolling to the top brings it back.

If you hide it, `hideOnScroll` shows the behavior to copy:

- **It follows the finger.** The bar moves pixel for pixel with the scroll until it has traveled its own height plus the safe area, and snaps to shown or hidden once scrolling stops. A boolean that flips on direction makes the bar jump on a 2px scroll.
- **It comes back** on scroll up, at the top, at the end of the content and whenever keyboard focus is inside it.
- **It moves a transform only.** The progress lives in a motion value, so scrolling never re-renders the bar or the page. A version that calls `setState` in the scroll handler renders on every scroll event.
- **Reduced motion** turns the slide into a flip after a full bar height of scrolling.

> Live demo (Bottom Tab Bar): Scroll the page: the floating bar follows your finger out of view and comes back as soon as you scroll up. Try it at [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) and install it with `npx wingo-ui@latest add bottom-tab-bar`.

## When is a sidebar the better choice?

When there are more than five top-level destinations, when they nest, or when most sessions happen on a laptop. Apple suggests "a sidebar or a tab bar that adapts to a sidebar as an alternative for an app with a complex information structure" and asks you to "Avoid overflow tabs"; Android recommends the bar for compact windows.

| Your app | On a phone | From 768px |
| --- | --- | --- |
| 3 to 5 sections, used daily on phones | Bottom tab bar | Sidebar or top navigation |
| 6 or more sections, or nested ones | The 4 most used in the bar, everything in a drawer | Sidebar |
| Admin tool used mostly at a desk | Drawer, optional tab bar | Sidebar with an icon rail |
| Content or docs site | Top bar and a menu | Top navigation |

Most apps need both, so the [App Shell](https://wingo-ui.com/components/app-shell) renders the Sidebar beside the page from 768px and, below it, the same sidebar in a drawer plus a tab bar for the daily destinations:

```tsx
// app/(app)/layout.tsx with the App Shell
import type { ReactNode } from "react";
import { AppShell } from "@/components/ui/app-shell";
import { Sidebar } from "@/components/ui/sidebar";
import { Topbar } from "@/components/ui/topbar";
import { nav, tabs } from "./nav";

export default function AppLayout({ children }: { children: ReactNode }) {
  return (
    <AppShell sidebar={<Sidebar groups={nav} filter />} topbar={<Topbar title="Summit CRM" />} tabs={tabs}>
      {children}
    </AppShell>
  );
}
```

`nav` holds every destination in groups and `tabs` the four people open every day. The shell renders the tab bar itself and reads the route from a `LinkProvider` (`component={Link}`, `currentPath={usePathname()}`) set once in a client provider; the [Next.js admin dashboard tutorial](https://wingo-ui.com/blog/nextjs-admin-dashboard-tutorial) builds that provider and the whole layout. The [Admin Shell](https://wingo-ui.com/components/admin-shell) adds a More tab that opens the drawer.

If you came looking for a shadcn mobile bottom navigation: as of October 2026, the shadcn/ui [component list](https://ui.shadcn.com/docs/components) has none, and its [Sidebar](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/sidebar.tsx) becomes an 18rem Sheet below 768px, two taps from any destination. Material UI's [Bottom Navigation](https://mui.com/material-ui/react-bottom-navigation/) is a fine react bottom navigation component, and the better pick if your app already runs on Material UI. As of October 2026 its docs cover labels, fixed positioning and router links, and leave badges, safe areas and hide on scroll to you.

## What breaks a bottom navigation bar in production?

- **Home is active on every page.** A hand-rolled `pathname.startsWith(href)` matches `/` everywhere. The bar matches `/` only exactly and picks the longest matching `href`, so `/clients/northwind-tech` keeps Clients active.
- **Tapping the active tab does nothing.** People expect it to scroll to the top or go back to the tab's first screen. `scrollToTopOnReselect` does both, and `onReselect` can refresh a feed.
- **A sixth tab.** The bar warns in development above five items. Move the rest into a More tab or the drawer.
- **Safari's floating toolbar.** Since iOS 26, Safari's toolbar floats over the bottom of the page in the Compact tab layout, and a [September 2025 forum report](https://developer.apple.com/forums/thread/800798) says Safari would not render fixed content below those controls. Test on a real iPhone in the Compact, Bottom and Top tab layouts.
- **Two bars, two insets.** A buy bar above the tab bar must not pad the home indicator again; the [mobile-first React components](https://wingo-ui.com/blog/mobile-first-react-components) guide shows the stacking.

## What should you install?

`npx wingo-ui@latest add bottom-tab-bar` copies the component into `components/ui/` with the Badge, the navigation and motion helpers and the scroll hooks it imports. The Bottom Tab Bar, FAB, Sidebar and App Shell are part of [Wingo UI Pro](https://wingo-ui.com/pricing); the Badge is free. More guides like this one live in [Mobile UI in React](https://wingo-ui.com/blog/category/mobile-ui).

## Components in this post

- [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) (Pro): The phone's primary navigation: 3 to 5 tabs at the thumb with an animated pill, badges, a raised center action and hide on scroll. Install: `npx wingo-ui@latest add bottom-tab-bar`
- [FAB](https://wingo-ui.com/components/fab) (Pro): The floating action button for a phone screen's main action, with a labelled or radial speed dial and a label that folds while you scroll. Install: `npx wingo-ui@latest add fab`
- [Sidebar](https://wingo-ui.com/components/sidebar) (Pro): The admin sidebar: grouped links with icons, nested sections, badges, a sliding active pill, an icon rail with flyouts and a filter. Install: `npx wingo-ui@latest add sidebar`
- [App Shell](https://wingo-ui.com/components/app-shell) (Pro): The whole admin layout in one component: a collapsible, resizable sidebar, a top bar and the page; a drawer and a tab bar on phones. Install: `npx wingo-ui@latest add app-shell`

## FAQ

### How many items should a bottom navigation bar have?

Three to five. Android's guidance lists three to five destinations of equal importance, and Apple's tab bar guidelines ask you to avoid overflow tabs. With more sections, keep the four most used in the bar and put everything in a drawer or a sidebar.

### Does shadcn/ui have a bottom navigation component?

Not as of October 2026: its component list has no bottom navigation or tab bar, and its Sidebar turns into an 18rem Sheet below 768px. Wingo UI's Bottom Tab Bar, a Pro component, installs the same way, as source code in your project, with npx wingo-ui@latest add bottom-tab-bar.

### Should a bottom navigation bar use role="tablist"?

No. The ARIA tabs pattern is for panels that switch inside one page. A bar that changes the page is navigation: a nav element with links, and aria-current="page" on the active one.

### Should a bottom navigation bar hide on scroll?

Only on long feeds and reading screens, because a hidden bar also hides where people are in the app. If it hides, it should follow the finger, move with a transform only, and come back on scroll up, at the top and end of the page, and while keyboard focus is inside it.

### Is the Wingo UI Bottom Tab Bar free?

No. The Wingo UI Bottom Tab Bar is a Pro component, like the FAB, the Sidebar and the App Shell; the Badge it uses for counts is free.

---

Source: https://wingo-ui.com/blog/react-bottom-navigation-bar
