@capgo/capacitor-native-navigation

中文文档
8.16.1发布于 2026/11/04MPL-2.0TypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 7.2万

Capacitor plugin for native navbar, tabbar, safe-area handling, and WebView snapshot transitions.

原始使用文档
文档目录48 节
  1. ➡️ Get started for free
  2. Demo
  3. iPhone Duo
  4. Navbar segment rows
  5. Pull to refresh
  6. Features
  7. Choose a surface
  8. How It Fits
  9. Modals, sheets, and popovers
  10. Compatibility
  11. Install
  12. Minimal Usage
  13. Native Tab Styling
  14. Android Liquid Glass
  15. Transition Flow
  16. Use With @capgo/capacitor-transitions
  17. Scroll-driven navbar
  18. Navbar search
  19. Tab bar placement and alignment
  20. Navbar segment rows (multi-level top bar)
  21. Zoom Transition
  22. CSS Insets
  23. Web Components
  24. Icon Descriptors
  25. Platform Notes
  26. Example App
  27. API
  28. configure(...)
  29. setNavbar(...)
  30. reportNavbarScroll(...)
  31. setPullToRefresh(...)
  32. endPullToRefresh()
  33. setTabbar(...)
  34. beginTransition(...)
  35. finishTransition(...)
  36. getPluginVersion()
  37. addListener('navbarBack', ...)
  38. addListener('navbarItemTap', ...)
  39. addListener('navbarSearch', ...)
  40. addListener('navbarSegmentSelect', ...)
  41. addListener('tabSelect', ...)
  42. addListener('safeAreaChanged', ...)
  43. addListener('verticalBarChanged', ...)
  44. addListener('hingeChanged', ...)
  45. addListener('transitionStart', ...)
  46. addListener('transitionEnd', ...)
  47. addListener('pullToRefresh', ...)
  48. Interfaces

@capgo/capacitor-native-navigation

Capgo - Build Capacitor apps with AI, ship updates instantly

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-navigation in 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

AppTap flowSVG iconsStyling
Demo of capacitor-native-navigation in actionAnimated native navigation tap flow showing tab selection, push transition, and native backAnimated native SVG icon demo showing inline SVG icons, native tint, labels, and tab selectionAnimated native navigation options demo showing dynamic colors, selected labels, custom indicators, badges, system Liquid Glass, and zoom transitions
iOS Liquid GlassAndroid blurAndroid tap flowCurved tabbar
iOS native Liquid Glass navbar and tabbar screenshotAndroid native navbar and floating tabbar over page content blurred with CSS backdrop-filter, in the Mimesis appAndroid recording of the Mimesis app: a large navbar title collapsing on scroll, a push transition with native back, and switching between three tabsAndroid native curved tabbar with raised center camera action and side margin

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, centerOpen, centerOpen, systemFolded, system
iPhone Duo simulator, folded: curved tab bar and a circular camera button stay on the bottomiPhone Duo simulator, open in landscape: the camera button stays a circle on the bottom bariPhone Duo simulator, open in landscape: system tab bar and navbar compose button in the vertical railiPhone Duo simulator, folded: system tab bar and navbar compose button in the vertical rail

iPhone Duo simulator, open in portrait: navbar and system tab bar stay horizontal

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.

iPhone Duo simulator, open in landscape: back icon, prominent Done, grouped reply and forward, and a detached search button in the vertical 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.

iOSAndroid
iOS recording: tapping Account sections, switching to Organization swaps the second row to eight scrolling chips, selecting BillingAndroid recording: segmented Account / Organization row over scrolling section chips, switching scope and selecting Billing

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.

iOSAndroid
iOS native pull to refresh below the navbarAndroid SwipeRefreshLayout pull to refresh below the navbar
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 reports navbarSearch.
  • 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, and tabSelect.
  • Enable an optional Android Liquid Glass-style translucent surface for native bars (pair it with a CSS backdrop-filter for 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, SwipeRefreshLayout on Android) with setPullToRefresh().
  • 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

SurfaceUse it for
Navbar searchFilter the page under a large title, as in Settings or a library.
Segment rowsSwitch scope and section on the current screen.
Floating bar, centerThe default phone tab bar.
Floating bar, start or endA short bar sitting toward one side.
SidebarThe same tabs as a trailing rail on iPad, iPhone Duo, and Android.
CurveA 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 shape is not curve and minimizeBehavior is not onScrollDown. The plugin calls UITabBarController.setTabBarHidden(_:animated:). The tab controller stays mounted and the WebView stays hosted in it. UIKit fades the bar. animated: false on 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 setTabbar on 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-top and --cap-native-navbar-height to 0. Scroll-linked navbar changes wait until the next setNavbar that shows the bar.
  • A hidden Android tabbar sets --cap-native-navigation-bottom and --cap-native-tabbar-height to 0, so the overlay can use the space the bar is leaving.
  • On the iOS 26+ system tab bar, --cap-native-tabbar-height is already 0: the WebView is hosted inside the tab and does not extend under the bar. An animated hide keeps --cap-native-system-tabbar-height at 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 restHide, then reveal
iPhone 17 simulator: navbar stays visible over the top of a long listiPhone 17 simulator: the navbar is hidden after the list scrolls down

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.

Settings search under the large title, light appearance Settings search under the large title, dark appearance

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:

  • auto lets iOS 18 and newer decide. Compact iPhone stays on the bottom. iPad and iPhone Duo can use the trailing sidebar.
  • bottom keeps a bottom bar.
  • sidebar asks 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.

iPad: floating tab bar aligned to the start iPad: sidebar placement shows the system 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-height is 0 for hosted system bars). On an iPhone Duo rail this is the rail thickness, and the overlap is --cap-native-navigation-left or --cap-native-navigation-right.
  • --cap-native-navigation-vertical-bar-edge (leading, trailing, or empty)
  • --cap-native-navigation-division-x and --cap-native-navigation-division-width (the fold, or 0px)

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, and UITabBarController so 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

Framework-agnostic native navigation chrome API.

configure(...)

configure(options?: NativeNavigationConfigureOptions | undefined) => Promise<NativeNavigationInsetsResult>

Configure the native chrome host and content inset behavior.

ParamType
optionsNativeNavigationConfigureOptions

Returns: Promise<NativeNavigationInsetsResult>


setNavbar(...)

setNavbar(options: NativeNavigationNavbarOptions) => Promise<NativeNavigationInsetsResult>

Render or update the native navbar.

ParamType
optionsNativeNavigationNavbarOptions

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.

ParamType
optionsNativeNavigationNavbarScrollEvent

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.

ParamType
optionsNativeNavigationPullToRefreshOptions

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.

ParamType
optionsNativeNavigationTabbarOptions

Returns: Promise<NativeNavigationInsetsResult>


beginTransition(...)

beginTransition(options?: NativeNavigationBeginTransitionOptions | undefined) => Promise<NativeNavigationTransitionResult>

Capture the current WebView and prepare a native transition.

ParamType
optionsNativeNavigationBeginTransitionOptions

Returns: Promise<NativeNavigationTransitionResult>


finishTransition(...)

finishTransition(options?: NativeNavigationFinishTransitionOptions | undefined) => Promise<NativeNavigationTransitionResult>

Animate from the captured WebView snapshot to the current live WebView.

ParamType
optionsNativeNavigationFinishTransitionOptions

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>
ParamType
eventName'navbarBack'
listenerFunc(event: NativeNavigationBackEvent) => void

Returns: Promise<PluginListenerHandle>


addListener('navbarItemTap', ...)

addListener(eventName: 'navbarItemTap', listenerFunc: (event: NativeNavigationBarItemTapEvent) => void) => Promise<PluginListenerHandle>
ParamType
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.

ParamType
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.

ParamType
eventName'navbarSegmentSelect'
listenerFunc(event: NativeNavigationNavbarSegmentSelectEvent) => void

Returns: Promise<PluginListenerHandle>

Since: 8.13.0


addListener('tabSelect', ...)

addListener(eventName: 'tabSelect', listenerFunc: (event: NativeNavigationTabSelectEvent) => void) => Promise<PluginListenerHandle>
ParamType
eventName'tabSelect'
listenerFunc(event: NativeNavigationTabSelectEvent) => void

Returns: Promise<PluginListenerHandle>


addListener('safeAreaChanged', ...)

addListener(eventName: 'safeAreaChanged', listenerFunc: (event: NativeNavigationSafeAreaChangedEvent) => void) => Promise<PluginListenerHandle>
ParamType
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.

ParamType
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.

ParamType
eventName'hingeChanged'
listenerFunc(event: NativeNavigationHingeChangedEvent) => void

Returns: Promise<PluginListenerHandle>

Since: 8.14.0


addListener('transitionStart', ...)

addListener(eventName: 'transitionStart', listenerFunc: (event: NativeNavigationTransitionEvent) => void) => Promise<PluginListenerHandle>
ParamType
eventName'transitionStart'
listenerFunc(event: NativeNavigationTransitionEvent) => void

Returns: Promise<PluginListenerHandle>


addListener('transitionEnd', ...)

addListener(eventName: 'transitionEnd', listenerFunc: (event: NativeNavigationTransitionEvent) => void) => Promise<PluginListenerHandle>
ParamType
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.

ParamType
eventName'pullToRefresh'
listenerFunc() => void

Returns: Promise<PluginListenerHandle>

Since: 8.11.0


Interfaces

NativeNavigationInsetsResult

Returned by methods that may change safe content bounds.

PropType
insetsNativeNavigationInsets

NativeNavigationInsets

Insets exposed to web content.

PropTypeDescriptionSince
topnumber
rightnumber
bottomnumber
leftnumber
navbarHeightnumber
tabbarHeightnumberBottom 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.
systemTabbarHeightnumberNominal 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 }).
verticalBarEdgeNativeNavigationVerticalBarEdge | nullSemantic 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
reservedRegionsNativeNavigationReservedRegion[]Fold and occlusion rectangles reported by iOS, in the root view's points. Empty when the device has no reserved regions.8.14.0
hingeNativeNavigationHinge | nullLatest 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.

PropType
kind'division' | 'occlusion'
xnumber
ynumber
widthnumber
heightnumber
activeboolean

NativeNavigationHinge

iPhone Duo hinge sample. angle is radians.

PropType
anglenumber
status'unknown' | 'closed' | 'partiallyOpen' | 'fullyOpen'

NativeNavigationConfigureOptions

Global plugin configuration.

PropTypeDescriptionSince
enabledbooleanEnables or disables the native chrome host.
platformStyleNativeNavigationPlatformStyleNative style preference. auto uses the current platform.
contentInsetModeNativeNavigationContentInsetModeWhen css, the plugin writes CSS variables on document.documentElement.
animationDurationnumberDefault native transition duration in milliseconds.
colorsNativeNavigationColorsShared color hints for native bars.

版本与发布记录

@capgo/capacitor-native-navigation 共发布 66 个版本,首次发布于 2026/05/11;过去 12 个月发布了 66 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
8.16.1latest2026/11/044 天前
8.16.02026/10/2911 天前
8.15.22026/10/2811 天前
8.15.12026/10/2713 天前
8.15.02026/10/2614 天前
8.14.02026/10/2515 天前

其他发布通道:pr-12 → 8.0.15-beta.pr12.18.1,beta → 8.0.15-beta.pr12.18.1。

关于 @capgo/capacitor-native-navigation 的常见问题

@capgo/capacitor-native-navigation 是什么?

Capacitor plugin for native navbar, tabbar, safe-area handling, and WebView snapshot transitions.

如何安装 @capgo/capacitor-native-navigation?

在项目目录运行 npm install @capgo/capacitor-native-navigation,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@capacitor/core(>=8.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@capgo/capacitor-native-navigation 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@capgo/capacitor-native-navigation 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@capgo/capacitor-native-navigation 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@capgo/capacitor-native-navigation 可以商用吗?

@capgo/capacitor-native-navigation 使用 MPL-2.0 许可。弱 Copyleft:可以商用,也可以和闭源代码一起使用;但如果分发时修改了 MPL 授权的文件,这些文件的修改需以 MPL 开源。以上为概括,不构成法律意见,请以许可原文为准。

@capgo/capacitor-native-navigation 的下载量有多少?

根据 npm 官方下载统计,@capgo/capacitor-native-navigation 在 2026/10/09 – 2026/11/07 这 30 天内被下载 72,012 次,日均约 2,400 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@capgo/capacitor-native-navigation 还在维护吗?

最新版本 8.16.1 发布于 2026/11/04(4 天前);至今共发布 66 个版本,其中过去 12 个月发布了 66 个;首次发布于 2026/05/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。