Skip to content

Navigation bar

Make the Android navigation bar match your screen: light or dark buttons, hidden for an immersive view. @symbiote-native/navigation-bar wraps expo-navigation-bar so every SymbioteNative adapter can drive it, not just React. The imperative functions are shared by every adapter. The declarative NavigationBar component and the useVisibility binding are ported to all five, in each framework’s own idiom.

This is Android only: upstream ships no iOS implementation, so every function throws UnavailabilityError on iOS. Guard calls with a platform check.

OS platform Support
iOS not applicable (throws)
Android live
Framework adapter Support
React live
Vue live
Angular live
Svelte live
Solid live
Terminal window
npm install @symbiote-native/navigation-bar

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --navigation-bar (or add --navigation-bar in an existing app) installs and wires this for you - see @symbiote-native/cli.

expo-navigation-bar and expo-modules-core come along as regular dependencies, pinned to exact versions. Never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

No permission or manifest edit is needed.

Render NavigationBar to set the style and visibility for as long as a screen is mounted. The deepest mounted NavigationBar wins on shared fields, and unmounting the last one restores the defaults.

import { NavigationBar, useVisibility } from '@symbiote-native/navigation-bar/react';
export default function Immersive() {
const visibility = useVisibility();
return (
<view>
<NavigationBar style="light" hidden />
<text>{visibility ?? 'loading'}</text>
</view>
);
}
import { addVisibilityListener, getVisibilityAsync, setHidden, setStyle } from '@symbiote-native/navigation-bar';
setStyle('dark');
setHidden(true);
const visibility = await getVisibilityAsync();
const subscription = addVisibilityListener(({ visibility }) => console.log(visibility));
// later:
subscription.remove();
Signature Description
setStyle(style): void Sets the button style. 'auto' and 'inverted' resolve against the current color scheme. Skips the native call when the resolved style is unchanged
setHidden(hidden): void Hides or shows the bar. Skips the native call when the value is unchanged
setVisibilityAsync(visibility): Promise<void> Sets visibility to 'visible' or 'hidden', calling native directly and bypassing setHidden’s dedupe
getVisibilityAsync(): Promise<INavigationBarVisibility> Reads the current visibility
addVisibilityListener(listener): EventSubscription Calls listener with { visibility, rawVisibility } when visibility changes
Prop Type Description
style 'auto' | 'inverted' | 'light' | 'dark' Button style. 'auto' follows the color scheme. Defaults to 'auto'
hidden boolean Whether the bar is hidden
Adapter Entry point Returns
React useVisibility() 'visible', 'hidden', or undefined while loading
Vue useVisibility() A ref of the same
Svelte useVisibility() An object with a reactive current
Solid createVisibility() An accessor of the same
Angular inject(NavigationBarVisibilityService).visibility A signal of the same
  • Every function throws UnavailabilityError off Android. This replaces upstream’s own console.warn and no-op fallback, matching how this repo’s other platform-gated packages behave.
  • Nested NavigationBar components merge by depth. The deepest mounted one wins on shared fields, so a modal can override its parent’s style and the parent’s value returns when the modal unmounts.
  • It can only be verified on an Android device or emulator. The headless tests fake the native module, so they prove the dedupe, the merge stack and each adapter’s lifecycle.
  • setBackgroundColorAsync has no effect. Reported on Expo Go and some device configurations; test in a development build.
  • “The current activity is no longer available”. A setter was called while no activity was attached (app backgrounded or starting). Retry after the app is foregrounded.
  • Passing a PlatformColor fails. The native side expects a plain color int; pass a color string.
  • Black bar with inset-swipe. Reported on phones that hide the bottom bar by default.
  • iOS. There is no navigation bar there; the module is Android only.

Sources: expo/expo#36814, expo/expo#33950, expo/expo#18515, expo/expo#36994.

expo-navigation-bar’s JS is hand-ported into this package’s core/, resolving the native module through expo-modules-core rather than the expo meta-package; the native resolution is Android-only and the base entry is an empty stub. The merge-stack logic lives once in core/entries-stack.ts; each adapter supplies only its own lifecycle glue over it. The native code is never vendored: expo-modules-autolinking resolves it from node_modules (see the native setup guide).