Skip to content
ShineUI
Install

BottomNav

Thumb-reach navigation.

<sui-bottom-nav>Menus and navigationStable

About

Mobile tab bar on the brand chrome for four or five destinations. The active tab carries the accent top bar and accent icon, badges sit on the icon, and every target is 56px tall. It is the phone-width partner of sui-side-nav; use one or the other, never both at once.

Usage

<sui-bottom-nav aria-label="Primary, Basic" active="Invoices"></sui-bottom-nav> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-bottom-nav"); el.items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Payments", icon: "fa-solid fa-circle-dollar-to-slot" }, { label: "More", icon: "fa-solid fa-ellipsis" }, ]; /* SuiBottomNavItem[] */ el.addEventListener("suiActiveChange", (e) => console.log(e.detail)); </script>
import { SuiBottomNav } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Payments", icon: "fa-solid fa-circle-dollar-to-slot" }, { label: "More", icon: "fa-solid fa-ellipsis" }, ]; /* SuiBottomNavItem[] */ return <SuiBottomNav aria-label="Primary, Basic" active="Invoices" items={items} onSuiActiveChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiBottomNav } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Payments", icon: "fa-solid fa-circle-dollar-to-slot" }, { label: "More", icon: "fa-solid fa-ellipsis" }, ]; // SuiBottomNavItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiBottomNav aria-label="Primary, Basic" active="Invoices" :items="items" @sui-active-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiBottomNav } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiBottomNav], template: `<sui-bottom-nav aria-label="Primary, Basic" [(active)]="active" [items]="items"></sui-bottom-nav>`, }) export class Example { active?: string | undefined; items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Payments", icon: "fa-solid fa-circle-dollar-to-slot" }, { label: "More", icon: "fa-solid fa-ellipsis" }, ]; // SuiBottomNavItem[] }

Basic

The bar is a nav landmark named Primary and each tab is a button in document order, so Tab reaches them and Enter or Space activates. The active tab carries aria-current="page", and every target clears the 44px minimum.

<div style="max-width:420px"><sui-bottom-nav aria-label="Primary, Basic" active="Invoices"></sui-bottom-nav></div>
// setup(root): properties and events setup(root) { root.querySelector('sui-bottom-nav').items = tabs(); }

Five tabs and badges

A badge sits on the icon and carries an open count, so it needs a destination that reports the same number on the page.

<div style="max-width:420px"><sui-bottom-nav aria-label="Primary, Five tabs and badges" id="bn-five" active="Bills"></sui-bottom-nav></div>
// setup(root): properties and events setup(root) { root.querySelector('#bn-five').items = [ { label: 'Home', icon: 'fa-solid fa-house' }, { label: 'Invoices', icon: 'fa-regular fa-file-lines', badge: 9 }, { label: 'Bills', icon: 'fa-regular fa-clipboard', badge: 3 }, { label: 'Reports', icon: 'fa-regular fa-chart-bar' }, { label: 'More', icon: 'fa-solid fa-ellipsis' }, ]; }

Disabled tab

A destination gated by plan or permission carries disabled rather than leaving the array, so the bar keeps its layout. It renders a native disabled button, which takes no focus and emits nothing.

<div style="max-width:420px"><sui-bottom-nav aria-label="Primary, Disabled tab" id="bn-off" active="Home"></sui-bottom-nav></div>
// setup(root): properties and events setup(root) { root.querySelector('#bn-off').items = [ { label: 'Home', icon: 'fa-solid fa-house' }, { label: 'Invoices', icon: 'fa-regular fa-file-lines', badge: 9 }, { label: 'Payroll', icon: 'fa-regular fa-money-bill-1', disabled: true }, { label: 'More', icon: 'fa-solid fa-ellipsis' }, ]; }

A tab with href is a real link, so open in a new tab works and the browser follows it on Enter. suiNavigate still fires and active still updates on the tap; a disabled tab stays a disabled button.

<div style="max-width:420px"><sui-bottom-nav aria-label="Primary, Links" id="bn-links" active="Home"></sui-bottom-nav></div>
// setup(root): properties and events setup(root) { root.querySelector('#bn-links').items = [ { label: 'Home', icon: 'fa-solid fa-house', href: '#/' }, { label: 'Invoices', icon: 'fa-regular fa-file-lines', href: '#/invoices', badge: 9 }, { label: 'Help', icon: 'fa-regular fa-circle-question', href: 'https://example.com/help', target: '_blank' }, { label: 'Payroll', icon: 'fa-regular fa-money-bill-1', href: '#/payroll', disabled: true }, ]; }

Events

Tapping a tab that is not active writes active and emits suiActiveChange; every tap then emits suiNavigate with the tab key, which is the item key when it has one and the label otherwise. Give every tab a key once the labels are translated. A disabled tab emits nothing.

On Home.
<div style="max-width:420px;display:flex;flex-direction:column;gap:10px"> <sui-bottom-nav aria-label="Primary, Navigate event" id="bn-events" active="Home"></sui-bottom-nav> <span id="bn-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">On Home.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#bn-log'); const el = root.querySelector('#bn-events'); el.items = tabs(); el.addEventListener('suiNavigate', (e) => { log.textContent = `Navigated to ${e.detail}.`; }); }

Keyboard

KeyAction
TabMoves through the tabs in order. A tab with href is a link and one without is a button; a disabled tab is skipped.
EnterActivates the tab: emits suiActiveChange when it was not active, then suiNavigate with its key, marks it aria-current, and a link tab then follows its href.
SpaceActivates a button tab. A link tab follows on Enter alone, as any link does.

API

<sui-bottom-nav>

Properties

PropertyAttributeTypeDefaultDescription
activeactivestringstring | undefinedKey of the active tab, its own key or its label. Mutable; defaults to the first tab; updated on tap and reported through suiActiveChange.
fixedfixedbooleanfalsePins the bar to the viewport bottom with the top corners rounded. Pair it with safe-area padding on real devices.
itemsproperty onlySuiBottomNavItem[][]Tabs to render: { label, icon, badge?, disabled?, href?, target? }[]. A tab the user may not reach carries disabled rather than leaving the array, so the bar keeps its layout; a tab with href is a real link. Property, not attribute.

Events

EventDetailReactVueDescription
suiActiveChangestringonSuiActiveChange@sui-active-changeFires whenever the element writes active, a tap on a tab that was not active, before suiNavigate; detail is the new active. Writing active from script emits nothing. Does not bubble.
suiNavigatestringonSuiNavigate@sui-navigateFires when a tab is activated; detail is the tab key, its own key or its label.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the active tab, or the first enabled one.

Types

SuiBottomNavItem interfaceOne tab: label, Font Awesome icon class, optional badge count.
labelstring
key?stringIdentity for active and suiNavigate. Defaults to the label; set it once the labels are translated or edited, so a handler keys off something that does not move.
iconstring
badge?string | number
disabled?booleanInert tab (a destination gated by permission or plan). Renders a native disabled button.
href?stringRenders the tab as a real link, so middle-click and open in a new tab work and Enter follows it. suiNavigate still fires and active still updates on the tap. A disabled tab stays a disabled button whatever href says.
target?stringLink target for a tab with href, e.g. _blank. A _blank tab gets rel="noopener".

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also