Documentation

Mobile Navigation

How VitNode's menus behave on phones - a floating bottom bar of up to three links from vitnode.config.ts plus a Menu tab for the rest, and a side drawer behind the user button for the account.

On a phone, the desktop header's navigation has nowhere to go. So below the sm breakpoint (640px) VitNode splits it in two: your most-used links move to a floating bottom bar pinned to the bottom of the screen - the kind your thumb already knows from native apps - and everything else lives in a side drawer behind the user button at the top.

Both menus come from the navigation option in src/vitnode.config.ts. See Main Navigation for the item options.

Two menus, one config

KeyWhere visitors see itHolds
headerIn the header on tablets and desktops, and in the side drawer on phonesAny number of items, with dropdowns one level deep
mobileBottomBarPinned to the bottom of the screen, on phones onlyUp to three links, no dropdowns, plus a Menu tab

A page can sit in both menus at once - Search in the header and in the bottom bar is a perfectly normal choice.

src/vitnode.config.ts
import { CompassIcon, MessagesSquareIcon, SearchIcon } from 'lucide-react'

export const vitNodeConfig = buildConfig({
  // ...
  navigation: {
    header: [
      // ...
    ],
    mobileBottomBar: [
      { href: '/forum', icon: MessagesSquareIcon, title: 'app.menu.forum' },
      { href: '/discover', icon: CompassIcon, title: 'app.menu.discover' },
      { href: '/search', icon: SearchIcon, title: 'app.menu.search' },
    ],
  },
})

A fourth link stops the app at startup with a [VitNode navigation] error, so the bar never silently drops one.

The bottom bar

Each link is an icon above a short label. The current page is tinted and sits on a highlight that slides between tabs. A tab counts as current when the URL is its address or anything below it, so /blog/article/my-first-post keeps a Blog tab lit (/ only matches the home page itself).

The last tab is always Menu. It opens a drawer from the bottom with the rest of the Header menu - everything the bar does not already show, dropdowns included. On a page that lives in there, the Menu tab is the one lit. If the bar already covers every header link, the Menu tab steps aside.

An empty bottom bar is not drawn at all. That is the default when navigation.mobileBottomBar is missing, so phones start with just the header and its side drawer until you add a link.

Give every link an icon and a short label

At 320px each of the four tabs gets about 72px, which fits one word like "Discover" or "Settings" - longer labels are cut off with an ellipsis. A link without an icon shows the first letter of its label instead. The two menus are separate lists, so a bottom bar link can point at a shorter message key than the same page in the header.

The side drawer

The user button stays at the top of the header, where it always was. On a phone it shows the member's avatar (or a person icon for guests), and a tap slides a drawer in from the side with:

  • the member's name and email, linking to their profile - or Login and Register buttons for guests,
  • the full Header menu, dropdowns included - only while the bottom bar is empty, since otherwise its Menu tab already has it,
  • the account links from the desktop user menu (profile, files, settings, AdminCP for admins),
  • theme and language switches,
  • Log out.

Swipe it away, tap outside it, press Esc or hit the close button. Desktop is untouched: the same button still opens the usual dropdown there.

For developers

  • No API involved. The header reads both menus from the config in the browser and on the server, so they render with the page, prerendered pages included.
  • Room at the bottom. While the bar is on screen, the page gets extra bottom padding so it never covers your footer.
  • Notches and home indicators. The bar sits above the iPhone home indicator via env(safe-area-inset-bottom), which needs viewport-fit=cover in your viewport meta tag. New apps have it; in an older app, update the viewport entry in src/routes/__root.tsx:
src/routes/__root.tsx
{ content: 'width=device-width, initial-scale=1, viewport-fit=cover', name: 'viewport' },
  • Translations. The drawer's own words - "Menu", "Navigation", "Account" - live under core.global.mobile_nav. See Languages & Localization to translate them. Link titles are message keys from your own locale files.