capacitor-navigation
v0.4.0
Published
Native navigation bars (nav bar + tab bar) and back event bridging for Capacitor
Downloads
518
Maintainers
Readme
capacitor-navigation
Native navigation primitives for Capacitor apps:
- Top navigation bar
- Bottom tab bar
- Back gesture/back press event bridge
- Layout metrics API for anchoring web UI to native chrome
This plugin is designed for SPA routers. Native bars are rendered by iOS/Android, and page transitions remain in your web layer.
Install
pnpm add capacitor-navigation
npx cap syncImport
import { Navigation } from 'capacitor-navigation'Quick Start
1) Show a native top bar
await Navigation.setNavigationBar({
title: 'Home',
subtitle: 'Native toolbar',
showBack: true,
shadow: true,
rightButtons: [
{ id: 'search', icon: 'search', label: 'Search' },
{ id: 'settings', icon: 'settings', label: 'Settings' },
],
})2) Show a native tab bar
await Navigation.setTabBar({
items: [
{ id: 'home', label: 'Home', icon: 'house', androidIcon: 'home' },
{ id: 'feed', label: 'Feed', icon: 'list.bullet', androidIcon: 'list' },
{ id: 'profile', label: 'Profile', icon: 'person', androidIcon: 'person' },
],
selected: 'home',
tintColor: '#007AFF',
darkTintColor: '#0A84FF',
insetContent: true, // content stays above tab bar
})3) Route in JS when native back happens
import { useRouter } from 'vue-router'
const router = useRouter()
const l1 = await Navigation.addListener('backPress', async () => {
if (window.history.length > 1) await router.back()
})
const l2 = await Navigation.addListener('backTap', async () => {
if (window.history.length > 1) await router.back()
})API
Methods
setNavigationBar(options)hideNavigationBar()setTabBar(options)updateTab({ id, badge?, label? })selectTab({ id })hideTabBar()setBackGesture(options)getLayoutMetrics()setStatusBar(options)setColorScheme({ scheme: 'light' | 'dark' | 'auto' })getColorScheme()→{ scheme: 'light' | 'dark' }removeAllListeners()
Events
backGesturebackGestureProgresswith{ state, progress, committed }backPressbackTaptabSelectwith{ id }navActionTapwith{ id }layoutMetricsChangewithNavigationLayoutMetricscolorSchemeChangewith{ scheme: 'light' | 'dark' }
Layout Metrics
Use native metrics to position floating web UI under header or above tab bar.
type NavigationLayoutMetrics = {
viewportWidth: number
viewportHeight: number
safeAreaTop: number
safeAreaBottom: number
navBarVisible: boolean
navBarTop: number
navBarHeight: number
navBarBottom: number
tabBarVisible: boolean
tabBarTop: number
tabBarHeight: number
tabBarBottom: number
contentTop: number
contentBottom: number
contentHeight: number
}Example:
const metrics = await Navigation.getLayoutMetrics()
const top = metrics.navBarBottom + 8
const bottomPanelTop = metrics.tabBarTop - 56 - 10The plugin also injects CSS variables:
--cap-nav-height--cap-tab-height--cap-content-top--cap-content-bottom--cap-content-height--cap-nav-bottom--cap-tab-top
Scroll Mode — Full vs Between Bars
By default, web content is inset between native bars (insetContent: true) so content never overlaps the bars. Set insetContent: false for full/under-bar scrolling.
await Navigation.setNavigationBar({ title: 'Home', insetContent: true })
await Navigation.setTabBar({ items: [...], insetContent: true })| Mode | Behavior |
|------|----------|
| insetContent: true (default) | Content is bounded between bars — no overlap |
| insetContent: false | Content extends under bars — bars float over content |
Platform implementation:
- iOS —
scrollView.contentInset(native, rubberbanding preserved) - Android —
WebView.setPadding() - Web —
bodypadding injected via<style>tag
Status Bar
Control the status bar icon color independently from the app color scheme — useful for full-screen hero pages or custom nav bars with dark backgrounds.
// White clock/icons (for dark backgrounds)
await Navigation.setStatusBar({ style: 'light' })
// Dark clock/icons (for light backgrounds)
await Navigation.setStatusBar({ style: 'dark' })
// Follow the active color scheme (default)
await Navigation.setStatusBar({ style: 'auto' })
// Hide status bar (iOS only)
await Navigation.setStatusBar({ hidden: true })
// Android: also set background color
await Navigation.setStatusBar({ style: 'light', backgroundColor: '#000000' })| Platform | style: 'light' | style: 'dark' | style: 'auto' |
|----------|-----------------|-----------------|-----------------|
| iOS | .lightContent (white icons) | .darkContent (dark icons) | .default |
| Android | isAppearanceLightStatusBars = false | = true | follows isDarkMode() |
iOS note: No
Info.plistchange required. The plugin installs a child view controller at runtime that takes ownership ofpreferredStatusBarStylevia the Objective-C runtime.
Dark / Light Mode
Dark color variants
Pass both light and dark color hex values; the native layer picks the right one automatically.
await Navigation.setTabBar({
items: [...],
tintColor: '#007AFF', // light mode active
darkTintColor: '#0A84FF', // dark mode active
unselectedTintColor: '#6C6C70', // light mode inactive
darkUnselectedTintColor: '#8E8E93', // dark mode inactive
backgroundColor: '#F9F9F9',
darkBackgroundColor: '#1C1C1E',
})
await Navigation.setNavigationBar({
title: 'Home',
tintColor: '#007AFF',
darkTintColor: '#0A84FF',
backgroundColor: '#F2F2F7',
darkBackgroundColor: '#1C1C1E',
titleColor: '#000000',
darkTitleColor: '#FFFFFF',
})iOS creates an adaptive
UIColor(dynamicProvider:)— colors update automatically without re-calling the method. Android readsisDarkMode()at call time; re-call aftercolorSchemeChangeto refresh.
Force dark / light from JS
// Force dark mode
await Navigation.setColorScheme({ scheme: 'dark' })
// Force light mode
await Navigation.setColorScheme({ scheme: 'light' })
// Follow system
await Navigation.setColorScheme({ scheme: 'auto' })
// Read current active scheme
const { scheme } = await Navigation.getColorScheme()
// 'light' | 'dark'| Platform | dark | light | auto |
|----------|------|-------|------|
| iOS 17+ / 26+ | UIWindowScene.traitOverrides.userInterfaceStyle = .dark + JS injection | .light | .unspecified |
| iOS 13–16 | UIWindow.overrideUserInterfaceStyle = .dark + JS injection | .light | .unspecified |
| Android | AppCompatDelegate.MODE_NIGHT_YES | MODE_NIGHT_NO | MODE_NIGHT_FOLLOW_SYSTEM |
| Web | html[data-color-scheme="dark"] + color-scheme: dark | light | removes attribute |
iOS also injects
html[data-color-scheme]+html.style.colorSchemeinto the WKWebView so thatprefers-color-schememedia queries and[data-color-scheme]CSS selectors both update reliably across all iOS versions.
React to system theme changes
await Navigation.addListener('colorSchemeChange', ({ scheme }) => {
// scheme: 'light' | 'dark'
// On Android: re-call setTabBar / setNavigationBar with correct colors
// On iOS: not needed if using darkTintColor etc. (adaptive colors auto-update)
applyTheme(scheme)
})Platform Notes
iOS
- Supports
iosStyle: 'auto' | 'ios18' | 'ios26' - Supports
blurStyle,backButtonStyle, and advanced back bubble tuning - Native edge pan emits progressive
backGestureProgress
Android (14+)
- Predictive back is enabled with native callback progress
- Back bubble is supported and configurable through
setBackGesture getLayoutMetricsandlayoutMetricsChangeare implemented natively- Tab bar icons use Material icon names (
androidIcon)
Option Highlights
setNavigationBar(options)
Common:
title,subtitle,visibleshowBack,shadowrightButtonorrightButtonstintColor,darkTintColorbackgroundColor,darkBackgroundColortranslucentuseSafeAreaTop,topInsetinsetContent— push web content below bar (true= between bars,false= full/under)
iOS-focused:
iosStyleblurStylebackButtonStyle,backButtonLabelbackButtonBackgroundColor,backButtonSize,backButtonCornerRadiustitleColor,darkTitleColor
setTabBar(options)
items: TabItem[]selectedtintColor,darkTintColorunselectedTintColor,darkUnselectedTintColorbackgroundColor,darkBackgroundColortranslucentuseSafeAreaBottom,bottomInsetvisibleinsetContent— push web content above tab bar (true= between bars,false= full/under)
TabItem:
id,labelicon(SF Symbol for iOS)androidIcon(Material icon name for Android)badge
setBackGesture(options)
enabledbubbleEnabledbubbleTintColorbubbleBackgroundColorcommitThresholdcommitVelocitybubbleSizebubbleRestingXbubbleEmergeDistancebubbleProgressDistancebubbleTopInsetbubbleBottomInsetbubbleMinScalebubbleMaxScalebubbleMinAlphabubbleMaxAlphabubbleAllowEdgeOverflow
Vue Example (Anchor UI To Native Bars)
import { computed, onMounted, ref } from 'vue'
import { Navigation } from 'capacitor-navigation'
import type { NavigationLayoutMetrics } from 'capacitor-navigation'
const metrics = ref<NavigationLayoutMetrics | null>(null)
onMounted(async () => {
metrics.value = await Navigation.getLayoutMetrics()
await Navigation.addListener('layoutMetricsChange', data => {
metrics.value = data
})
})
const headerAccessoryStyle = computed(() => {
if (!metrics.value) return { display: 'none' }
return { top: `${Math.round(metrics.value.navBarBottom + 8)}px` }
})
const bottomAccessoryStyle = computed(() => {
if (!metrics.value) return { display: 'none' }
return { top: `${Math.round(metrics.value.tabBarTop - 56 - 10)}px` }
})Behavior Model
- Native bars are UI chrome only
- Routing remains in your SPA router
- Back events are emitted to JS; your app decides when/how to navigate
