#The arcade nav bar

The top bar is global. It's injected into every game at build time, so your game does not draw its own header, and it already carries the brand, the section links, sign-in, the player's Gamesight Score and the notification bell.

Two things it can't know: that your game has 50 coins and 15 cards, and that the player is halfway through a match they'd rather not lose. Arcade.nav covers both.

#Your game's drawer

Opt in and you get a drawer hanging off the bottom-left of the bar: your game's name and up to four live values. It's only as wide as its contents, so it reads as part of the bar rather than as a second nav. (Buttons get a second drawer of their own, on the right — see below.)

Arcade.nav.open({
  name: "Sightlines",
  icon: "♟",                                     // emoji or an image URL
  stats: [
    { icon: "🪙", value: 50, label: "Currency" },
    { icon: "🂠", value: 15, label: "Cards in collection" },
  ],
});
Field
name Heading on the left. Defaults to your game slug.
icon Emoji, or an image URL (anything starting / or http). Optional.
stats[].icon Same rules. Optional.
stats[].value The number or short string, shown bold.
stats[].label Tooltip and accessible name. Not visible text, so value has to stand alone.
actions Up to three buttons. They get a drawer of their own; see below.

Updating and closing:

Arcade.nav.stats([{ icon: "🪙", value: 42 }]);   // values only, name/icon kept
Arcade.nav.clear();                              // close the drawer

#The actions drawer

A second drawer opens under the account menu, on the right, for the game's own controls. It's separate from the identity drawer on the left on purpose: that one says what this is and what the player has, this one says what they can do here.

The bar covers the top of the viewport, so a button floating up there ends up underneath it. This drawer is the one strip guaranteed clear, and it takes up to three buttons:

Arcade.nav.actions([
  { label: "Cancel", icon: "←", onClick: () => closeEditor() },
  { label: "Save deck", icon: "💾", onClick: save, primary: true },
]);
Field
label A word or two. The drawer is not a toolbar.
icon Emoji or image URL, before the label. Optional.
onClick Called on click. Not used with menu or panel.
disabled Greyed out and unclickable, for a control you can't honour yet (mid-save, say).
primary The accent fill. For the one action that commits.
menu Makes it a dropdown of rows: { label, icon?, onClick, disabled? }.
panel Makes it a dropdown you fill yourself. See below.

open() replaces the whole action list, so a game that reopens the drawer per screen asks for its buttons after rendering that screen. That is what makes the top-level menu the one screen with no Back.

#A dropdown you fill yourself

panel hands you an empty, arcade-styled dropdown each time it opens. Return a cleanup function and it runs on close. This is how an action gets a form without costing a page — Sightlines invites a player from here rather than sending them to a challenge screen:

Arcade.nav.actions([{
  label: "Invite player",
  icon: "⚔️",
  panel(el) {
    el.innerHTML = '<input type="search" placeholder="Search players…">';
    const q = el.querySelector("input");
    let timer;
    q.addEventListener("input", () => {
      clearTimeout(timer);
      timer = setTimeout(async () => {
        const found = await Arcade.players.search(q.value);
        /* render rows; Arcade.invites.send({ to, kind: "match" }) on click */
      }, 250);
    });
    return () => clearTimeout(timer);          // called when the panel closes
  },
}]);

The arcade owns the furniture — the panel's frame, the text field, the row hover, dismissal on Escape and on a click outside. You own what goes in it. The first focusable element is focused for you.

#Collapse it, if the bar is in the way

A full-viewport game — most engine exports — can ask for the bar to start out of the way. One line in game.json:

"navBar": "collapsed"     // default is "expanded"

The bar then parks off-screen and leaves a small handle at the top-left. Clicking it slides the bar in; clicking again (or Escape) sends it back. It collapses to a handle rather than disappearing, because a player still needs a way back to the arcade.

Two reasons this is a manifest setting and not an SDK call:

  • It applies from the first paint, so there's no flash of a full bar over your game while your code boots.
  • An engine export can't easily call JS at startup. A line of JSON works whatever you built the game in.

While collapsed, --arcade-nav-h is the height of the handle (1.9rem), not the bar — so the strip the arcade reserves shrinks to match, and a game clearing it only clears what actually overlaps.

#Don't let a stray click bin a match

The bar floats over your game, which means "Arcade" and "Docs" are a couple of pixels from your playfield. Guard the parts of your game that would lose something:

Arcade.nav.guard("Leaving now abandons this match, and it counts as a loss.");
// ... when the match ends:
Arcade.nav.guard(false);

Turn it on when there's something to lose and off the moment there isn't. Two routes out are covered:

  • Links that leave the game, including the bar's own. You get a confirm with your wording, so the player is told what's at stake.
  • Closing or reloading the tab. The browser asks, but browsers substitute their own generic wording and ignore yours. Nothing to be done about that.

Links that stay inside /games/<your-slug>/, in-page anchors, target="_blank" and downloads all pass through without a prompt.

Arcade.nav.guard(true) uses generic wording if you don't have anything specific to say, but a reason is worth writing.

#Leave room for the bar

The bar is position: fixed over your game, because every game in this repo lays out against 100vh and wouldn't make room for a bar in normal flow.

The strip is reserved for you. On a game page the arcade sets padding-top: var(--arcade-nav-h) on <body>, so ordinary page flow starts below the bar and its drawer. Your top row needs no padding of its own.

Two things that still need you:

/* 1. A shell sized to the viewport: subtract the strip, or the page scrolls. */
#app { min-height: calc(100vh - var(--arcade-nav-h, 0px)); }

/* 2. Anything you take out of flow — a fixed HUD, a full-screen overlay — is
      unaffected by the body padding, so clear the bar yourself. */
.my-hud-row { top: calc(var(--arcade-nav-h, 4.6rem) + 0.5rem); }

Keep the fallbacks, and note they differ: 0px where you're subtracting (a game in its own dev server has no bar and should fill the window) and 4.6rem where you're offsetting (so the rule still looks right standalone). --arcade-nav-h covers the bar and both drawers, and is re-measured when a drawer changes and on resize — on a narrow window the two drawers stack, and the value grows to match.

The bar sits at z-index: 500. Keep your game's own stacking below that. A full-screen overlay on top of the bar takes away the player's only route back to the arcade, which is the one thing the bar exists to guarantee.

#What not to build

Everything here is already in the bar, and a second copy will overlap it rather than sit beside it:

  • A brand, or a link home
  • Sign-in, the player's name or avatar
  • The Gamesight Score
  • The notification bell. Arcade.notifications.mount exists for pages the arcade doesn't control; in a game the bar already has one.
  • A leaderboard screen, or an achievements screen. Every game gets /leaderboard/<slug>/ and /achievements/<slug>/ built for it. Link out.
  • A rules or how-to screen, if you ship games/<slug>/docs/. That becomes /docs/games/<slug>/, which is one page to keep current instead of two.

Sightlines is the worked example. It used to draw a full header of its own, which overlapped the arcade's once the bar went global. Now it opens a drawer with two values, puts every screen's Back button in it, and guards its matches; the match drawer's action is "Leave match" instead. Its own leaderboard and how-to screens are gone too, in favour of /leaderboard/sightlines/ and /docs/games/sightlines/. See pushNavStats, navBack and guardMatch in games/sightlines/src/main.ts.

See also: SDK reference · achievements · multiplayer

View docs/guides/nav-bar.md on GitHub ↗