Skip to content

Components

Nav bar

The app's two bars: the bottom tab bar with a notch for the centre action, and the top bar with the location pill, the logo and the date stepper.

Preview

Variants

Pick a variant with the controls. The preview and both code blocks follow.

Variant
Active tab
Centre action
Variant.tsx
<AppNavBar />

Implementation

The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.

Usage
<AppNavBar /><AppNavBar variant="bottom" active="forecast" action={false} /><AppNavBar variant="top" />
app-nav-bar.tsx · barPath
/** The bar's outline: rounded corners (--radius-f21) and the notch cut for the centre action, as NavBarClipper draws it. */function barPath(w: number): string {  const c = w / 2;  const r = F.f21;  return `M${r} 0 H${c - NOTCH_HALF} C${c - F.f34} 0 ${c - F.f34} ${NOTCH_DEPTH} ${c} ${NOTCH_DEPTH} C${c + F.f34} ${NOTCH_DEPTH} ${c + F.f34} 0 ${c + NOTCH_HALF} 0 H${w - r} A${r} ${r} 0 0 1 ${w} ${r} V${BAR_H - r} A${r} ${r} 0 0 1 ${w - r} ${BAR_H} H${r} A${r} ${r} 0 0 1 0 ${BAR_H - r} V${r} A${r} ${r} 0 0 1 ${r} 0 Z`;}
app-nav-bar.tsx · AppNavBar
/** * The app's two navigation bars. "bottom" is the tab bar with a notch for the centre action: four tabs, the active * one lit with a glow under its label, and the map action in the notch. "top" is the header: the location pill, the * logo, and the date stepper. */export function AppNavBar({  variant = "bottom",  active: initial = "home",  action = true,}: {  variant?: "bottom" | "top";  active?: NavTab;  /** Show the centre action in the notch (bottom bar). */  action?: boolean;}) {  const [active, setActive] = useState<NavTab>(initial);  const [ref, w] = useElementWidth<HTMLDivElement>(377);  if (variant === "top") {    return (      <div className="app-ui app-navbar is-top">        <button type="button" className="app-ui app-button is-glass is-sm">          <Glyph name="pin" />          <span className="app-navbar-loc">Current Location</span>        </button>        {/* eslint-disable-next-line @next/next/no-img-element */}        <img className="app-navbar-logo" src="/astr-icon.png" alt="Astr" />        <div className="app-navbar-date">          <button type="button" className="app-ui app-tile is-sm" aria-label="Previous day"><Glyph name="chevron-left" /></button>          <button type="button" className="app-ui app-button is-glass is-sm">Oct 10</button>          <button type="button" className="app-ui app-tile is-sm" aria-label="Next day"><Glyph name="chevron-right" /></button>        </div>      </div>    );  }  return (    <div ref={ref} className="app-ui app-navbar is-bottom" style={{ height: BAR_H }}>      <svg viewBox={`0 0 ${w} ${BAR_H}`} height={BAR_H} aria-hidden="true">        <defs>          <linearGradient id="nav-sheen" x1="0" x2="1" y1="0" y2="1">            <stop offset="0" stopColor={stop("space-grey-50", 5)} />            <stop offset="1" stopColor={stop("space-grey-50", 8)} />          </linearGradient>          <linearGradient id="nav-shine" gradientUnits="userSpaceOnUse" x1="0" x2="0" y1="0" y2={NOTCH_DEPTH}>            <stop offset="0" stopColor={stop("deep-space-200", 10)} />            <stop offset="0.5" stopColor={stop("deep-space-200", 1)} />            <stop offset="1" stopColor={stop("deep-space-200", 10)} />          </linearGradient>        </defs>        <path d={barPath(w)} fill={stop("space-grey-950", 1)} />        <path d={barPath(w)} fill="url(#nav-sheen)" stroke={stop("space-grey-50", 5)} />        <path d={notchPath(w)} fill="none" stroke="url(#nav-shine)" strokeWidth={F.f2} strokeLinecap="round" />      </svg>      <nav aria-label="Primary" className="app-navbar-items">        {TABS.slice(0, 2).map((t) => (          <NavItem key={t.id} tab={t} active={active === t.id} onSelect={setActive} />        ))}        <span className="app-navbar-gap" aria-hidden="true" />        {TABS.slice(2).map((t) => (          <NavItem key={t.id} tab={t} active={active === t.id} onSelect={setActive} />        ))}      </nav>      {action && (        <button type="button" className="app-ui app-tile is-round app-navbar-action" aria-label="Sky map">          <Glyph name="map" />        </button>      )}    </div>  );}
app-nav-bar.tsx · NavItem
function NavItem({ tab, active, onSelect }: { tab: { id: NavTab; label: string; glyph: GlyphName }; active: boolean; onSelect: (t: NavTab) => void }) {  return (    <div className="app-navbar-tab" data-active={active || undefined} onClick={() => onSelect(tab.id)}>      <button type="button" className="app-ui app-tile is-sm" aria-label={tab.label} aria-current={active ? "page" : undefined} aria-pressed={active}>        <Glyph name={tab.glyph} />      </button>      <span className="app-micro" aria-hidden="true">{tab.label}</span>    </div>  );}
app-ui.css
.app-navbar { position: relative; width: min(100%, var(--container-f377)); margin-inline: auto; }.app-navbar.is-bottom > svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }.app-navbar-items { position: absolute; inset: 0; display: flex; align-items: center; justify-content: space-between; padding: 0 var(--spacing-f21); }.app-navbar-gap { flex: 0 0 var(--spacing-f89); }.app-tile.is-round { border-radius: var(--radius-full); }.app-navbar-tab { display: grid; justify-items: center; gap: var(--spacing-f3); cursor: pointer; }.app-navbar-tab[data-active] .app-micro,.app-navbar-tab:has(.app-tile:active) .app-micro { color: var(--c-text); text-shadow: 0 var(--spacing-f1) var(--spacing-f13) var(--c-accent), 0 0 var(--spacing-f1) color-mix(in srgb, var(--c-text) var(--mag-3), transparent); }.app-navbar-action { position: absolute; left: 50%; top: calc(var(--spacing-f55) / -2); transform: translateX(-50%); }.app-navbar.is-top { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-f8); padding: var(--spacing-f8) var(--spacing-f13); border-radius: var(--radius-f21); border: var(--spacing-f1) solid var(--hair); background: var(--sheen), var(--fill); box-shadow: var(--highlight); box-sizing: border-box; }.app-navbar-date { display: flex; align-items: center; gap: var(--spacing-f5); }

API reference

React propFlutter parameterTypeDefaultMeaning
variantthe two bars of ScaffoldWithNavBarbottom, topbottomThe tab bar with a notch, or the header.
activenavigationShell.currentIndexhome, objects, forecast, settingshomeThe selected tab. The web bar keeps its own state when you tap.
actionfloatingActionButtonbooleantrueThe centre action in the notch (the sky map).

Design tokens

Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.

RoleCSS tokenDart tokenApp widget today
Bottom bar height--spacing-f89AstrSpace.f8970 + safe area
Notchdepth --spacing-f34, half-width --spacing-f55AstrSpace.f34, f55depth 42, half-width 50
Surface--sheen over space-grey-950 at --mag-1AstrColors.spaceGrey[950], Mag.m1AppColors.surfaceGlass #CC0C0F11
Taban icon tile (--spacing-f34) with its label (--text-s-2) under itAstrGlassTile(size: AstrTileSize.sm)icon 24, label 8, no tile
Active tabtile selected in deep-space-200, label lit with a deep-space-200 glowselected: true, AstrTone.deepSpacetextPrimary, #3A7FFF shadow
Inactive tabtile in space-grey-300tone space-grey-300textSubdued #7D8792
Notch shinedeep-space-200, --spacing-f2 strokeMag.m1AppColors.accent, 2
Centre actiona round tile, --spacing-f55AstrGlassTile, circularFAB 52, circle
Top barpills --spacing-f34 tall, --radius-fullAstrSize.controlCompact30, radius 30