Skip to main content
WireKit
Copy for LLM

Bottom Nav

The tab bar app-shaped mobile layouts live on. WireKit's layout family had a navbar, a sidebar, a header and a footer — but nothing pinned to the bottom of a phone, which is where a thumb actually is.

Bottom navigation with an active tab and a badge

Reserving room for it

The bar is position: fixed, so page content scrolls underneath it. Without room reserved, the last thing on every page hides behind the nav.

Add wk-bottom-nav-offset to whatever scrolls:

<main class="wk-bottom-nav-offset">
    …
</main>

<x-wirekit::bottom-nav label="Main">…</x-wirekit::bottom-nav>

That is opt-in on purpose: this component cannot know which element scrolls in your layout, and padding the wrong box is worse than padding none. The offset disappears at md along with the bar itself — add wk-bottom-nav-offset-always if you kept the bar on desktop with :mobile-only="false".

Items

Each item is a link with an icon, a label and an optional badge:

<x-wirekit::bottom-nav label="Main">
    <x-wirekit::bottom-nav.item href="/" label="Home" icon="home" :active="request()->is('/')" />
    <x-wirekit::bottom-nav.item href="/inbox" label="Inbox" icon="envelope" badge="3" />
</x-wirekit::bottom-nav>

The badge goes into the item's accessible name — "Inbox, 3" — because "Inbox" and "Inbox, 3" are different claims and a reader who cannot see the dot deserves the number. The visual badge is then hidden from assistive tech, so it is announced once rather than twice.

Where it shows

By default the bar hides from md up, where a sidebar or a navbar does this job — a bar pinned to the bottom of a 27-inch screen is a long way from the content it belongs to.

Pass :mobile-only="false" to keep it everywhere.

Props

<x-wirekit::bottom-nav>

Prop Type Default Description
label string 'Main' Names the nav landmark.
mobileOnly bool true Hide from md up.
interactive bool false Track the current tab client-side: clicking an item marks it active (no page load), keyed by label. Off by default — a real app usually drives active from its router.
scope string|null null Class-scope override.

<x-wirekit::bottom-nav.item>

Prop Type Default Description
href string '#' Where it goes.
label string '' Visible label, and the base of the accessible name.
active bool false Marks this as the current page.
icon string|null null Icon name.
badge string|int|null null Count on the icon. Joins the accessible name.
scope string|null null Class-scope override.

Accessibility

  • A real <nav> landmark with a name, so a screen-reader user can jump to it.
  • The current tab carries aria-current="page" and a marker line above it — the color is a third signal, never the only one (WCAG 1.4.1).
  • Items are at least 52px tall. A nav item is the primary control here, and a 32px tab on a phone is a tap people miss (WCAG 2.5.5).
  • The bar pads itself by env(safe-area-inset-bottom). The bottom of the viewport is not the bottom of the usable screen on a phone with a home indicator — without that padding the labels sit under it and the swipe-up gesture eats the taps.
  • The badge is announced once, as part of the item's name.

Keyboard Interaction

Key Action
Tab Move through the tabs in order.
Enter Follow the focused tab.

The items are ordinary links, so they keep every behavior a link has — middle-click, open in a new tab, the status bar showing where it goes.