@capgo/capacitor-native-navigation
Capgo: push fixes to your Capacitor users in minutes, build signed iOS and Android apps without a Mac, and roll back in one click.
➡️ Get started for free
14-day unlimited free trial. No credit card required
🤖 Building with an AI coding agent? Install Capgo skills so Claude Code, Codex, Cursor, Copilot, or Gemini CLI know how to install and use
@capgo/capacitor-native-navigationin your Capacitor app:npx skills add Cap-go/capgo-skills. Then ask your agent: "Add @capgo/capacitor-native-navigation to my Capacitor app and wire it up." Agent setup guides · Hosted MCP
Native navbar, tab bar, safe-area handling, and WebView snapshot transitions for Capacitor apps. Your web framework keeps routing and page rendering while this plugin owns the platform surfaces: a search field under the large title, a tab bar aligned to the start, center, or end, and a trailing sidebar rail.
iOS 27 and iPhone Duo. On iPhone Duo, iOS moves the status bar and the system tab bar into one vertical rail on the folded outer display and on the open display in landscape. Open portrait keeps the horizontal bars. Navbar icon buttons join that rail, and the title stays horizontal. A back or close icon sits at the top. Search can be a detached button, prominent actions stay pinned, and buttons that share a group collapse into one control that expands. The curved center bar stays on the bottom. configure({ verticalBarBehavior: 'disabled' }) keeps the horizontal bars for a screen that should not use the rail.
Demo
| App | Tap flow | SVG icons | Styling |
|---|---|---|---|
![]() | ![]() | ![]() |
| iOS Liquid Glass | Android blur | Android tap flow | Curved tabbar |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
The Android captures come from Mimesis on Android 16: a large navbar title that collapses on scroll, a translucent native tabbar and a transparent navbar over CSS backdrop-filter blur.
iPhone Duo
Folded, and open in landscape, the system tab bar becomes a vertical rail. Open portrait stays a horizontal bar. Split View puts each window's rail on the outer edge. The rail stays on the camera's physical edge, so the semantic edge is trailing in landscape and can be leading or trailing in Split View and in right-to-left layout. The curved center bar stays on the bottom, and its cradle stays a circle around the button when the window widens.
Icon buttons from the navbar join that rail. A text title stays horizontal, so a back control needs display: 'icon' (or role: 'close') before it sits at the top of the rail. dock: 'detached' draws a button on its own. dock: 'prominent' pins it past overflow and search. axis: 'vertical' puts a text button such as Done on the rail. Items that share group collapse to the first item, and the system expands the rest. search.placement: 'button' is a detached search button on the iPhone Duo rail in iOS 26 and later. A horizontal navbar, Android, and web keep the stacked field. verticalBarCompression: 'preferItems' keeps navbar buttons when the rail runs out of room. Segment rows stay horizontal and clear the rail. Curve and other custom capsules stay on the bottom, and the center button stays a circle. configure({ verticalBarBehavior: 'disabled' }) opts that screen out and keeps the top and bottom bars. Omitting verticalBarBehavior later keeps the current choice. safeAreaChanged reports verticalBarEdge, fold reservedRegions, and the latest hinge sample. verticalBarChanged and hingeChanged fire when those change.
await NativeNavigation.configure({ verticalBarBehavior: 'automatic' });
await NativeNavigation.setNavbar({
title: 'Notes',
verticalBarCompression: 'preferItems',
backButton: { visible: true, title: 'Back', display: 'icon' },
rightItems: [
{ id: 'done', title: 'Done', dock: 'prominent', axis: 'vertical' },
{ id: 'reply', title: 'Reply', icon: { ios: { sfSymbol: 'arrowshape.turn.up.left' } }, group: 'message' },
{ id: 'forward', title: 'Forward', icon: { ios: { sfSymbol: 'arrowshape.turn.up.right' } }, group: 'message' },
],
search: { placement: 'button', placeholder: 'Search' },
});
.page {
padding-inline-start: var(--cap-native-navigation-left);
padding-inline-end: var(--cap-native-navigation-right);
}
--cap-native-navigation-division-x and --cap-native-navigation-division-width mark the fold so a web layout can split around it. The plugin still uses one WebView. On a phone with no rail those inset variables are 0px.
Captured on the iPhone Duo simulator in iOS 27.1.
| Folded, center | Open, center | Open, system | Folded, system |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |

Open landscape with the navbar controls in the rail. The back icon sits under the status items, Done is a prominent text button, reply and forward share one group, and search is a detached button. The system tabs stay at the bottom of the rail.

Navbar segment rows
Multi-level native segment rows under the navbar: a scope switcher on top, section chips below it. See Navbar segment rows.
| iOS | Android |
|---|---|
![]() | ![]() |
Pull to refresh
Opt-in native pull-to-refresh, shown below the navbar. It works whether the page scrolls window or an inner root such as #app.
| iOS | Android |
|---|---|
![]() | ![]() |
await NativeNavigation.setPullToRefresh({ enabled: true });
NativeNavigation.addListener('pullToRefresh', async () => {
await reloadData();
await NativeNavigation.endPullToRefresh();
});
Features
- Drive native top navigation and bottom tabs from JavaScript state.
- Show a native search field under the navbar with
setNavbar({ search }). It collapses with a large title and reportsnavbarSearch. - Align a floating tab bar to the start, center, or end, and place the same tabs in a trailing sidebar rail.
- Use system-owned iOS navigation bars, tab bars, tab gestures, and Liquid Glass rendering.
- Emit native intent events such as
navbarBack,navbarItemTap,navbarSearch, andtabSelect. - Enable an optional Android Liquid Glass-style translucent surface for native bars (pair it with a CSS
backdrop-filterfor blur). - Capture WebView snapshots for native-feeling push, back, root, tab, and zoom transitions.
- Configure tab labels, selected icons, badges, indicators, ripples, tint colors, and dynamic colors.
- Write CSS inset variables so web content can scroll behind native bars without being hidden.
- On iPhone Duo, follow the system vertical rail: navbar items join it, safe areas clear it, and a screen can opt out.
- Stack multi-level native segment rows under the navbar (scope switcher plus section tabs) with
setNavbar({ segments }). - Opt into native pull-to-refresh (system spinner and haptic on iOS,
SwipeRefreshLayouton Android) withsetPullToRefresh(). - Hide the navbar and tabbar while a web modal, sheet, or popover is open. An animated tabbar hide keeps the iOS tab controller and WebView in place.
- Work with React, Vue, Angular, Svelte, Solid, vanilla JS, and any router that exposes imperative navigation.
Choose a surface
| Surface | Use it for |
|---|---|
| Navbar search | Filter the page under a large title, as in Settings or a library. |
| Segment rows | Switch scope and section on the current screen. |
| Floating bar, center | The default phone tab bar. |
| Floating bar, start or end | A short bar sitting toward one side. |
| Sidebar | The same tabs as a trailing rail on iPad, iPhone Duo, and Android. |
| Curve | A bottom bar with a raised center action. It stays on the bottom. |
How It Fits
- Your router still owns route state and page rendering.
- The app still uses one full-screen WebView.
- Icons must be serializable descriptors such as SVG strings, SF Symbols, or native resource names.
Modals, sheets, and popovers
Native bars are drawn above the WebView. A modal, sheet, dialog, or popover from the page (ion-modal, ion-popover, <dialog>, a menu) renders inside that WebView, so it stays underneath the navbar and the tabbar.
Hide each bar the overlay would meet, then send the same options again when the overlay closes. Count open overlays when they can stack. The inner dismiss has to leave the bars hidden while an outer overlay is still up.
let overlayCount = 0;
let navbar = {
title: 'Home',
backButton: { visible: false },
};
let tabbar = {
selectedId: 'home',
tabs: [
{ id: 'home', title: 'Home' },
{ id: 'settings', title: 'Settings' },
],
};
async function suspendNativeChrome() {
overlayCount += 1;
if (overlayCount > 1) {
return;
}
await NativeNavigation.setNavbar({ ...navbar, hidden: true });
await NativeNavigation.setTabbar({ ...tabbar, hidden: true, animated: true });
}
async function resumeNativeChrome() {
overlayCount = Math.max(0, overlayCount - 1);
if (overlayCount > 0) {
return;
}
await NativeNavigation.setNavbar({ ...navbar, hidden: false });
await NativeNavigation.setTabbar({ ...tabbar, hidden: false, animated: true });
}
Keep navbar and tabbar as the options for the current route, and update them in the same place you already call setNavbar and setTabbar. Call suspendNativeChrome() before the overlay presents (ionModalWillPresent, ionPopoverWillPresent, or the line before dialog.showModal()). Call resumeNativeChrome() after it is gone (ionModalDidDismiss, ionPopoverDidDismiss, or the dialog close event).
setTabbar({ hidden: false }) without tabs clears the bar. Spread the last options you rendered and change hidden.
Pass animated: true on that tabbar toggle:
- iOS 26+ system Liquid Glass, when
shapeis notcurveandminimizeBehavioris notonScrollDown. The plugin callsUITabBarController.setTabBarHidden(_:animated:). The tab controller stays mounted and the WebView stays hosted in it. UIKit fades the bar.animated: falseon this bar tears the controller down and moves the WebView back to the bridge, which runs during the modal's own animation. - Earlier iOS, the curved bar, and the minimize-on-scroll capsule. The bar fades over 200ms and stays mounted.
- Android. The bar and its backdrop slide and fade over 200ms, then leave the layout. Showing them plays that in reverse. A later
setTabbaron a bar that is already visible does not replay the slide-in.
The navbar hide is immediate on iOS and Android. animated on setNavbar applies to title and button changes while the bar is visible.
Insets update when the call resolves, while the tabbar animation is still running:
- A hidden navbar sets
--cap-native-navigation-topand--cap-native-navbar-heightto0. Scroll-linked navbar changes wait until the nextsetNavbarthat shows the bar. - A hidden Android tabbar sets
--cap-native-navigation-bottomand--cap-native-tabbar-heightto0, so the overlay can use the space the bar is leaving. - On the iOS 26+ system tab bar,
--cap-native-tabbar-heightis already0: the WebView is hosted inside the tab and does not extend under the bar. An animated hide keeps--cap-native-system-tabbar-heightat the nominal bar height (bar plus home-indicator padding). A non-animated hide drops that value as well, because the tab controller is gone.
A popover that stays clear of the bars can leave them up. Hide the navbar for a menu under the title, and the tabbar for a sheet that covers the bottom of the screen.
configure({ enabled: false }) turns the chrome host off. Use hidden for an overlay. On the web there is no native bar; the same calls still update the CSS inset variables.
Keep the modal in this WebView. The overlay is page state, and the router stays in charge of it.
Compatibility
@capgo/capacitor-native-navigation targets Capacitor 8 and Node.js 22+. iOS 27 is supported, including iPhone Duo.
Install
You can use our AI-Assisted Setup to install the plugin. Add the Capgo skills to your AI tool using the following command:
npx skills add https://github.com/cap-go/capacitor-skills --skill capacitor-plugins
Then use the following prompt:
Use the `capacitor-plugins` skill from `cap-go/capacitor-skills` to install the `@capgo/capacitor-native-navigation` plugin in my project.
If you prefer Manual Setup, install the plugin by running the following commands and follow the platform-specific instructions below:
npm install @capgo/capacitor-native-navigation
npx cap sync
Minimal Usage
import { NativeNavigation } from '@capgo/capacitor-native-navigation';
await NativeNavigation.configure({
contentInsetMode: 'css',
animationDuration: 360,
});
await NativeNavigation.setNavbar({
title: 'Home',
subtitle: 'Native chrome',
transparent: true,
scrollBehavior: 'both',
scrollThreshold: 8,
backButton: { visible: false },
rightItems: [
{
id: 'compose',
title: 'Compose',
icon: {
svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>',
},
},
],
});
await NativeNavigation.setTabbar({
selectedId: 'home',
labelVisibilityMode: 'labeled',
icons: true,
colors: {
dynamic: true,
},
tabs: [
{
id: 'home',
title: 'Home',
icon: {
svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 10v10h14V10"/></svg>',
},
},
{
id: 'settings',
title: 'Settings',
icon: {
svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>',
},
},
],
});
await NativeNavigation.addListener('tabSelect', ({ id }) => {
router.navigate(`/${id}`);
});
Native Tab Styling
await NativeNavigation.setTabbar({
selectedId: 'home',
labelVisibilityMode: 'selected',
indicatorColor: '#0A84FF',
rippleColor: '#330A84FF',
badgeBackgroundColor: '#FF3B30',
badgeTextColor: '#FFFFFF',
colors: {
dynamic: true,
tint: '#0A84FF',
inactiveTint: '#8E8E93',
},
tabs: [
{
id: 'home',
title: 'Home',
icon: { ios: { sfSymbol: 'house' }, android: { resource: 'ic_home' } },
selectedIcon: { ios: { sfSymbol: 'house.fill' }, android: { resource: 'ic_home_filled' } },
},
],
});
Android Liquid Glass
On Android 12+, floating tabbars use liquidGlass automatically when setTabbar omits glass (pass glass: { effect: 'none' } for a flat fill). Android draws a translucent tint without live blur: rendering the WebView from native code to blur it flashes the page or swallows flings. For blur, place an element with backdrop-filter: blur(...) in your page under the bar (the floating tabbar is 64px tall, 24px from the sides, at most 430px wide, 10px above the system navigation bar). Tune colors.background alpha and glass.surfaceAlpha (default 0.62) together. When the system font size is increased, the tab bar grows with the label line height and reported tabbarHeight / bottom insets stay aligned with the native bar.
await NativeNavigation.configure({
glass: {
effect: 'liquidGlass',
blurRadius: 18,
surfaceAlpha: 0.62,
},
});
await NativeNavigation.setTabbar({
selectedId: 'home',
tabs,
colors: {
background: '#F8FFFFFF',
},
});
Transition Flow
const transition = await NativeNavigation.beginTransition({ direction: 'forward' });
router.navigate('/detail');
await router.ready?.();
await NativeNavigation.setNavbar({
title: 'Detail',
backButton: { visible: true, title: 'Back' },
});
await NativeNavigation.finishTransition({
id: transition.id,
direction: 'forward',
});
Use With @capgo/capacitor-transitions
Use @capgo/capacitor-native-navigation for the native navbar, tabbar, safe-area insets, and native intent events. Use @capgo/capacitor-transitions for the WebView page stack underneath that native chrome.
npm install @capgo/capacitor-native-navigation @capgo/capacitor-transitions
npx cap sync
Initialize both packages once when the app starts:
import { NativeNavigation } from '@capgo/capacitor-native-navigation';
import '@capgo/capacitor-transitions';
import { initTransitions, setupRouterOutlet, setDirection } from '@capgo/capacitor-transitions/react';
initTransitions({ platform: 'auto' });
const outlet = document.querySelector('cap-router-outlet');
if (outlet) {
setupRouterOutlet(outlet, { platform: 'auto', swipeGesture: 'auto' });
}
await NativeNavigation.configure({
contentInsetMode: 'css',
});
Keep the transition outlet focused on pages. Do not render a web header or footer when native chrome owns those surfaces:
<cap-router-outlet platform="auto" swipe-gesture="auto">
<cap-page>
<cap-content slot="content" fullscreen>
<main class="page">Inbox content</main>
</cap-content>
</cap-page>
</cap-router-outlet>
.page {
min-height: 100dvh;
padding-top: var(--cap-native-navigation-top);
padding-bottom: var(--cap-native-navigation-bottom);
}
Drive both packages from the same router actions:
async function openMessage(id: string) {
setDirection('forward');
await router.push(`/messages/${id}`);
await NativeNavigation.setNavbar({
title: 'Message',
backButton: { visible: true, title: 'Inbox' },
});
}
await NativeNavigation.addListener('navbarBack', () => {
setDirection('back');
router.back();
});
await NativeNavigation.addListener('tabSelect', ({ id }) => {
setDirection('root');
router.push(`/${id}`);
});
Pick one animation layer per navigation. For normal route pushes, let @capgo/capacitor-transitions animate the WebView pages and update native bars with setNavbar / setTabbar. For shared-element or zoom routes, use beginZoomTransition / finishZoomTransition from this plugin and skip the web page transition for that navigation.
Scroll-driven navbar
Opt in per screen with setNavbar({ scrollBehavior: 'both' }). Defaults stay none, so existing apps keep static chrome. Native iOS and Android listen to scroll on window, #app, and any element marked with data-cap-native-navigation-scroll. Use reportNavbarScroll() only for custom scroll roots.
The example app route Scroll header demo (home tile) shows a long list with scrollBehavior: 'both'. Scroll down to hide the bar. Scroll up to bring it back before the list reaches the top.
| At rest | Hide, then reveal |
|---|---|
![]() | ![]() |
Navbar search
Pass search to setNavbar to draw a native field under the bar and above any segment rows. The row is 52pt on iOS and 52dp on Android, and that height is included in the top inset. iOS uses a UISearchBar. Android uses a text field. The web preview draws the same field above the page.
hidesWhenCollapsed defaults to true when the navbar is a large title. Once that title collapses, the row height becomes 0 and the top inset shrinks with it. On the web preview the field hides with the navbar when scroll behavior collapses the bar. Large-title collapse itself is native.
import { NativeNavigation } from '@capgo/capacitor-native-navigation';
let query = '';
const renderNavbar = () =>
NativeNavigation.setNavbar({
title: 'Settings',
large: true,
search: {
placeholder: 'Search',
text: query,
hidesWhenCollapsed: true,
},
});
await NativeNavigation.addListener('navbarSearch', async ({ text, reason }) => {
query = reason === 'cancel' ? '' : text;
await renderNavbar();
});
await renderNavbar();
Send the latest text on the next setNavbar so the field stays in sync. While the field is focused, an unchanged text is left alone. On iOS and Android, omitting search removes the field. The web implementation keeps the previous search object when the call omits it, so pass search: { hidden: true } to clear it there too.
The example Settings tab uses this field to filter an inset grouped list, and its appearance control switches the demo between light, dark, and the system scheme. The Navbar playground has a Search field toggle.
iPhone 17 simulator, iOS 27.0.
Tab bar placement and alignment
style.alignment places a floating bar inside the horizontal margin: start, center (the default), or end. start and end follow layoutDirection. Curve bars and sidebar rails ignore alignment.
style.placement chooses the chrome:
autolets iOS 18 and newer decide. Compact iPhone stays on the bottom. iPad and iPhone Duo can use the trailing sidebar.bottomkeeps a bottom bar.sidebarasks for the trailing rail. On iOS 18 and newer that is the system tab bar in sidebar mode, including on iOS 18 through 25. iPad portrait shows that mode as a top tab bar with the sidebar button. A compact iPhone may stay on the bottom when UIKit does not offer a sidebar. Android draws an 84dp rail, inset 12dp from the trailing edge, starting below the navbar. The navbar stays full width. The web preview reports the same left or right inset.
curve always stays on the bottom. sidebar ignores minimizeBehavior and drag-to-select.
await NativeNavigation.setTabbar({
tabs,
selectedId: 'home',
style: {
shape: 'floating',
alignment: 'end',
placement: 'bottom',
},
});
await NativeNavigation.setTabbar({
tabs,
selectedId: 'home',
style: {
shape: 'floating',
placement: 'sidebar',
},
});
The example Tabbar playground and the Settings tab both expose alignment and placement. Pad page content with --cap-native-navigation-left and --cap-native-navigation-right so a rail does not cover it.
iPad Pro 11-inch simulator, iOS 27.0. Alignment moves the floating capsule; the iOS 26 system tab bar does not. Sidebar placement uses UIKit's top tab bar and sidebar button.
Navbar segment rows (multi-level top bar)
Pass segments to setNavbar to stack rows of native segments under the bar, such as a scope switcher on top and section tabs below it. Rows draw top to bottom, each one adds 44pt (iOS) or 48dp (Android) to the navbar and to the reported insets. iOS uses UISegmentedControl / glass pills, Android uses Material segmented buttons and filter chips.
import { NativeNavigation } from '@capgo/capacitor-native-navigation';
const sections = {
account: ['General', 'Notifications', 'Password', 'Two-factor'],
organization: ['General', 'Members', 'Plans', 'Usage', 'Credits', 'Billing', 'Security', 'Webhooks'],
};
const selection = { scope: 'account', section: 'general' };
const renderNavbar = () =>
NativeNavigation.setNavbar({
title: 'Settings',
segments: [
{
id: 'scope',
selectedId: selection.scope,
items: [
{ id: 'account', title: 'Account' },
{ id: 'organization', title: 'Organization' },
],
},
{
id: 'section',
style: 'scroll', // horizontally scrolling chips; `auto` picks `segmented` for up to 4 items
selectedId: selection.section,
items: sections[selection.scope].map((title) => ({ id: title.toLowerCase(), title })),
},
],
});
await NativeNavigation.addListener('navbarSegmentSelect', async ({ groupId, id }) => {
if (groupId === 'scope') {
selection.scope = id;
selection.section = 'general'; // the second level depends on the first one
} else {
selection.section = id;
}
await renderNavbar();
});
await renderNavbar();
Selection moves natively when the user taps, but every setNavbar call applies selectedId again. Keep the selected ids in your state and pass them on every later setNavbar call; to reject a selection, send the previous selectedId. Items accept icon, accessibilityLabel and enabled like navbar buttons. Pass segments: [] to remove the rows.
The example app Navbar playground has a Segment rows toggle with this exact setup.
Zoom Transition
import { beginZoomTransition, finishZoomTransition } from '@capgo/capacitor-native-navigation';
const card = document.querySelector('[data-photo-card]');
if (card) {
const transition = await beginZoomTransition(card, { cornerRadius: 18 });
router.navigate('/photo/42');
await router.ready?.();
await finishZoomTransition(undefined, {
id: transition.id,
cornerRadius: 18,
});
}
CSS Insets
With contentInsetMode: 'css', the plugin updates these variables on document.documentElement:
.app-scroll {
height: 100dvh;
overflow: auto;
padding-top: calc(var(--cap-native-navigation-top) + 24px);
padding-left: var(--cap-native-navigation-left);
padding-right: var(--cap-native-navigation-right);
scroll-padding-bottom: calc(var(--cap-native-navigation-bottom) + 24px);
}
.page {
min-height: 100dvh;
padding-bottom: calc(var(--cap-native-navigation-bottom) + 24px);
}
Available variables:
--cap-native-navigation-top--cap-native-navigation-right--cap-native-navigation-bottom--cap-native-navigation-left--cap-native-navbar-height--cap-native-tabbar-height--cap-native-system-tabbar-height(nominal tab bar height; use on iOS when--cap-native-tabbar-heightis0for hosted system bars). On an iPhone Duo rail this is the rail thickness, and the overlap is--cap-native-navigation-leftor--cap-native-navigation-right.--cap-native-navigation-vertical-bar-edge(leading,trailing, or empty)--cap-native-navigation-division-xand--cap-native-navigation-division-width(the fold, or0px)
Web Components
The package can register optional custom elements for framework-agnostic declarative setup:
import { defineNativeNavigationElements } from '@capgo/capacitor-native-navigation';
defineNativeNavigationElements();
<cap-native-navigation-provider enabled="true" content-inset-mode="css"></cap-native-navigation-provider>
<cap-native-navbar
title="Settings"
large
search='{"placeholder":"Search","hidesWhenCollapsed":true}'
></cap-native-navbar>
<cap-native-tabbar
selected-id="home"
tabs='[{"id":"home","title":"Home","icon":{"ios":{"sfSymbol":"house.fill"}}}]'
tabbar-style='{"shape":"floating","alignment":"center","placement":"auto"}'
></cap-native-tabbar>
Icon Descriptors
const icon = {
svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10.5 12 3l9 7.5"/></svg>',
width: 24,
height: 24,
template: true,
src: 'shared_asset_name',
ios: {
svg: '<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/></svg>',
sfSymbol: 'house.fill',
image: 'BundledAssetName',
},
android: {
svg: '<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/></svg>',
resource: 'ic_menu_view',
image: 'bundled_drawable_name',
},
};
Inline SVG supports the icon-focused subset used by common sets such as Lucide and Feather: path, line, polyline, polygon, circle, and rect. The SVG is rendered as a template image by default, so native tint colors can recolor it without bundling a platform asset.
Platform Notes
- iOS uses UIKit
UINavigationBar,UISearchBar,UITabBar, andUITabBarControllerso the system owns tab interaction, Liquid Glass rendering, sidebar placement on iOS 18+, and safe-area behavior. - Android uses an AppCompat
Toolbar, a text field under that toolbar, and a native tab surface. Sidebar placement is a trailing rail. The navbar stays full width. - Web mirrors inset variables, the search field, sidebar insets, floating-bar alignment, and events for local development.
Example App
The example-app/ folder is a vanilla JS Capacitor demo linked with file:...
cd example-app
npm install
npm run build
npx cap add ios
npx cap add android
npx cap sync
API
configure(...)setNavbar(...)reportNavbarScroll(...)setPullToRefresh(...)endPullToRefresh()setTabbar(...)beginTransition(...)finishTransition(...)getPluginVersion()addListener('navbarBack', ...)addListener('navbarItemTap', ...)addListener('navbarSearch', ...)addListener('navbarSegmentSelect', ...)addListener('tabSelect', ...)addListener('safeAreaChanged', ...)addListener('verticalBarChanged', ...)addListener('hingeChanged', ...)addListener('transitionStart', ...)addListener('transitionEnd', ...)addListener('pullToRefresh', ...)- Interfaces
- Type Aliases
Framework-agnostic native navigation chrome API.
configure(...)
configure(options?: NativeNavigationConfigureOptions | undefined) => Promise<NativeNavigationInsetsResult>
Configure the native chrome host and content inset behavior.
| Param | Type |
|---|---|
options | NativeNavigationConfigureOptions |
Returns: Promise<NativeNavigationInsetsResult>
setNavbar(...)
setNavbar(options: NativeNavigationNavbarOptions) => Promise<NativeNavigationInsetsResult>
Render or update the native navbar.
| Param | Type |
|---|---|
options | NativeNavigationNavbarOptions |
Returns: Promise<NativeNavigationInsetsResult>
reportNavbarScroll(...)
reportNavbarScroll(options: NativeNavigationNavbarScrollEvent) => Promise<void>
Report scroll samples for scrollBehavior. Use only for scroll roots that
are not already wired by setNavbar (window, #app, or
[data-cap-native-navigation-scroll]). iOS, Android, and the web fallback
install those listeners automatically when scrolling is enabled.
| Param | Type |
|---|---|
options | NativeNavigationNavbarScrollEvent |
setPullToRefresh(...)
setPullToRefresh(options: NativeNavigationPullToRefreshOptions) => Promise<void>
Enable or disable the native pull-to-refresh gesture. Listen for
pullToRefresh, reload your data, then call endPullToRefresh() to hide
the spinner. No-op on web.
| Param | Type |
|---|---|
options | NativeNavigationPullToRefreshOptions |
Since: 8.11.0
endPullToRefresh()
endPullToRefresh() => Promise<void>
Stop the native pull-to-refresh spinner once the refresh is done.
Since: 8.11.0
setTabbar(...)
setTabbar(options: NativeNavigationTabbarOptions) => Promise<NativeNavigationInsetsResult>
Render or update the native tabbar.
| Param | Type |
|---|---|
options | NativeNavigationTabbarOptions |
Returns: Promise<NativeNavigationInsetsResult>
beginTransition(...)
beginTransition(options?: NativeNavigationBeginTransitionOptions | undefined) => Promise<NativeNavigationTransitionResult>
Capture the current WebView and prepare a native transition.
| Param | Type |
|---|---|
options | NativeNavigationBeginTransitionOptions |
Returns: Promise<NativeNavigationTransitionResult>
finishTransition(...)
finishTransition(options?: NativeNavigationFinishTransitionOptions | undefined) => Promise<NativeNavigationTransitionResult>
Animate from the captured WebView snapshot to the current live WebView.
| Param | Type |
|---|---|
options | NativeNavigationFinishTransitionOptions |
Returns: Promise<NativeNavigationTransitionResult>
getPluginVersion()
getPluginVersion() => Promise<PluginVersionResult>
Returns the platform implementation version marker.
Returns: Promise<PluginVersionResult>
addListener('navbarBack', ...)
addListener(eventName: 'navbarBack', listenerFunc: (event: NativeNavigationBackEvent) => void) => Promise<PluginListenerHandle>
| Param | Type |
|---|---|
eventName | 'navbarBack' |
listenerFunc | (event: NativeNavigationBackEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener('navbarItemTap', ...)
addListener(eventName: 'navbarItemTap', listenerFunc: (event: NativeNavigationBarItemTapEvent) => void) => Promise<PluginListenerHandle>
| Param | Type |
|---|---|
eventName | 'navbarItemTap' |
listenerFunc | (event: NativeNavigationBarItemTapEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener('navbarSearch', ...)
addListener(eventName: 'navbarSearch', listenerFunc: (event: NativeNavigationNavbarSearchEvent) => void) => Promise<PluginListenerHandle>
Fired when the navbar search field changes.
| Param | Type |
|---|---|
eventName | 'navbarSearch' |
listenerFunc | (event: NativeNavigationNavbarSearchEvent) => void |
Returns: Promise<PluginListenerHandle>
Since: 8.14.0
addListener('navbarSegmentSelect', ...)
addListener(eventName: 'navbarSegmentSelect', listenerFunc: (event: NativeNavigationNavbarSegmentSelectEvent) => void) => Promise<PluginListenerHandle>
Fired when the user selects an item in a navbar segment row.
| Param | Type |
|---|---|
eventName | 'navbarSegmentSelect' |
listenerFunc | (event: NativeNavigationNavbarSegmentSelectEvent) => void |
Returns: Promise<PluginListenerHandle>
Since: 8.13.0
addListener('tabSelect', ...)
addListener(eventName: 'tabSelect', listenerFunc: (event: NativeNavigationTabSelectEvent) => void) => Promise<PluginListenerHandle>
| Param | Type |
|---|---|
eventName | 'tabSelect' |
listenerFunc | (event: NativeNavigationTabSelectEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener('safeAreaChanged', ...)
addListener(eventName: 'safeAreaChanged', listenerFunc: (event: NativeNavigationSafeAreaChangedEvent) => void) => Promise<PluginListenerHandle>
| Param | Type |
|---|---|
eventName | 'safeAreaChanged' |
listenerFunc | (event: NativeNavigationSafeAreaChangedEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener('verticalBarChanged', ...)
addListener(eventName: 'verticalBarChanged', listenerFunc: (event: NativeNavigationVerticalBarChangedEvent) => void) => Promise<PluginListenerHandle>
Fired when the iPhone Duo vertical rail appears, disappears, or changes
edge. safeAreaChanged carries the same edge on every inset update.
| Param | Type |
|---|---|
eventName | 'verticalBarChanged' |
listenerFunc | (event: NativeNavigationVerticalBarChangedEvent) => void |
Returns: Promise<PluginListenerHandle>
Since: 8.14.0
addListener('hingeChanged', ...)
addListener(eventName: 'hingeChanged', listenerFunc: (event: NativeNavigationHingeChangedEvent) => void) => Promise<PluginListenerHandle>
Fired when the iPhone Duo hinge changes, or when hinge updates stop.
Prefer hinge.status over hinge.angle when a coarse pose is enough.
| Param | Type |
|---|---|
eventName | 'hingeChanged' |
listenerFunc | (event: NativeNavigationHingeChangedEvent) => void |
Returns: Promise<PluginListenerHandle>
Since: 8.14.0
addListener('transitionStart', ...)
addListener(eventName: 'transitionStart', listenerFunc: (event: NativeNavigationTransitionEvent) => void) => Promise<PluginListenerHandle>
| Param | Type |
|---|---|
eventName | 'transitionStart' |
listenerFunc | (event: NativeNavigationTransitionEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener('transitionEnd', ...)
addListener(eventName: 'transitionEnd', listenerFunc: (event: NativeNavigationTransitionEvent) => void) => Promise<PluginListenerHandle>
| Param | Type |
|---|---|
eventName | 'transitionEnd' |
listenerFunc | (event: NativeNavigationTransitionEvent) => void |
Returns: Promise<PluginListenerHandle>
addListener('pullToRefresh', ...)
addListener(eventName: 'pullToRefresh', listenerFunc: () => void) => Promise<PluginListenerHandle>
Fired when the user pulls to refresh. The spinner keeps spinning until
endPullToRefresh() is called.
| Param | Type |
|---|---|
eventName | 'pullToRefresh' |
listenerFunc | () => void |
Returns: Promise<PluginListenerHandle>
Since: 8.11.0
Interfaces
NativeNavigationInsetsResult
Returned by methods that may change safe content bounds.
| Prop | Type |
|---|---|
insets | NativeNavigationInsets |
NativeNavigationInsets
Insets exposed to web content.
| Prop | Type | Description | Since |
|---|---|---|---|
top | number | ||
right | number | ||
bottom | number | ||
left | number | ||
navbarHeight | number | ||
tabbarHeight | number | Bottom overlap inset for web layout when the WebView does not already reserve space for the native tab bar. On iOS 26+ with a system-hosted tab bar this stays 0 so content is not lifted twice. | |
systemTabbarHeight | number | Nominal native tab bar height (bar plus home-indicator padding on iOS) even when tabbarHeight is 0. Stays available while the tab bar is soft-hidden with setTabbar({ hidden: true, animated: true }). | |
verticalBarEdge | NativeNavigationVerticalBarEdge | null | Semantic edge of the vertical rail, or null when the bars are horizontal. On a rail, left or right carries the rail thickness and tabbarHeight stays 0 when the WebView is already beside it. systemTabbarHeight is that thickness rather than a bottom bar. | 8.14.0 |
reservedRegions | NativeNavigationReservedRegion[] | Fold and occlusion rectangles reported by iOS, in the root view's points. Empty when the device has no reserved regions. | 8.14.0 |
hinge | NativeNavigationHinge | null | Latest hinge sample. null when the device has no hinge. Angle updates are delivered on hingeChanged and are not pushed through this snapshot on every tick. | 8.14.0 |
NativeNavigationReservedRegion
A region iOS reserves inside the window, such as the fold or an occlusion.
| Prop | Type |
|---|---|
kind | 'division' | 'occlusion' |
x | number |
y | number |
width | number |
height | number |
active | boolean |
NativeNavigationHinge
iPhone Duo hinge sample. angle is radians.
| Prop | Type |
|---|---|
angle | number |
status | 'unknown' | 'closed' | 'partiallyOpen' | 'fullyOpen' |
NativeNavigationConfigureOptions
Global plugin configuration.
| Prop | Type | Description | Since |
|---|---|---|---|
enabled | boolean | Enables or disables the native chrome host. | |
platformStyle | NativeNavigationPlatformStyle | Native style preference. auto uses the current platform. | |
contentInsetMode | NativeNavigationContentInsetMode | When css, the plugin writes CSS variables on document.documentElement. | |
animationDuration | number | Default native transition duration in milliseconds. | |
colors | NativeNavigationColors | Shared color hints for native bars. |
















