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 |
Installation
Section titled “Installation”npm install @symbiote-native/navigation-barScaffolding 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> );}<script setup lang="ts">import { NavigationBar, useVisibility } from '@symbiote-native/navigation-bar/vue';
const visibility = useVisibility();</script>
<template> <view> <NavigationBar style="light" :hidden="true" /> <text>{{ visibility ?? 'loading' }}</text> </view></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { NavigationBar, NavigationBarVisibilityService,} from '@symbiote-native/navigation-bar/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS, NavigationBar], template: ` <view> <navigation-bar [style]="'light'" [hidden]="true" /> <text>{{ visibility() ?? 'loading' }}</text> </view> `,})export class Immersive { readonly visibility = inject(NavigationBarVisibilityService).visibility;}The component’s selector is navigation-bar. visibility is a signal.
<script lang="ts"> import { NavigationBar, useVisibility } from '@symbiote-native/navigation-bar/svelte';
const visibility = useVisibility();</script>
<view> <NavigationBar style="light" hidden={true} /> <text>{visibility.current ?? 'loading'}</text></view>Svelte returns an object with a reactive current.
import { NavigationBar, createVisibility } from '@symbiote-native/navigation-bar/solid';
export function Immersive() { const visibility = createVisibility();
return ( <view> <NavigationBar style="light" hidden /> <text>{visibility() ?? 'loading'}</text> </view> );}Solid reserves use* for consuming existing state, so the primitive is createVisibility.
Imperative calls
Section titled “Imperative calls”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();Functions
Section titled “Functions”| 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 |
NavigationBar props
Section titled “NavigationBar props”| 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 |
Visibility binding
Section titled “Visibility binding”| 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
UnavailabilityErroroff Android. This replaces upstream’s ownconsole.warnand no-op fallback, matching how this repo’s other platform-gated packages behave. - Nested
NavigationBarcomponents 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.
Common questions
Section titled “Common questions”setBackgroundColorAsynchas 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
PlatformColorfails. 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.
How the wrapper works
Section titled “How the wrapper works”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).