@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Package.swift +23 -0
- package/README.md +53 -2
- package/RdlaboCapacitorNativeUi.podspec +17 -0
- package/android/build.gradle +69 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +3 -0
- package/dist/native/activation.d.ts +14 -0
- package/dist/native/activation.d.ts.map +1 -0
- package/dist/native/activation.js +305 -0
- package/dist/native/activation.js.map +1 -0
- package/dist/native/components/index.d.ts +19 -0
- package/dist/native/components/index.d.ts.map +1 -0
- package/dist/native/components/index.js +38 -0
- package/dist/native/components/index.js.map +1 -0
- package/dist/native/components/ion-back-button.d.ts +10 -0
- package/dist/native/components/ion-back-button.d.ts.map +1 -0
- package/dist/native/components/ion-back-button.js +21 -0
- package/dist/native/components/ion-back-button.js.map +1 -0
- package/dist/native/components/ion-button.d.ts +10 -0
- package/dist/native/components/ion-button.d.ts.map +1 -0
- package/dist/native/components/ion-button.js +25 -0
- package/dist/native/components/ion-button.js.map +1 -0
- package/dist/native/components/ion-buttons.d.ts +11 -0
- package/dist/native/components/ion-buttons.d.ts.map +1 -0
- package/dist/native/components/ion-buttons.js +30 -0
- package/dist/native/components/ion-buttons.js.map +1 -0
- package/dist/native/components/ion-fab.d.ts +10 -0
- package/dist/native/components/ion-fab.d.ts.map +1 -0
- package/dist/native/components/ion-fab.js +132 -0
- package/dist/native/components/ion-fab.js.map +1 -0
- package/dist/native/components/ion-menu-button.d.ts +13 -0
- package/dist/native/components/ion-menu-button.d.ts.map +1 -0
- package/dist/native/components/ion-menu-button.js +44 -0
- package/dist/native/components/ion-menu-button.js.map +1 -0
- package/dist/native/components/ion-segment.d.ts +11 -0
- package/dist/native/components/ion-segment.d.ts.map +1 -0
- package/dist/native/components/ion-segment.js +30 -0
- package/dist/native/components/ion-segment.js.map +1 -0
- package/dist/native/components/ion-tab-bar.d.ts +11 -0
- package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
- package/dist/native/components/ion-tab-bar.js +50 -0
- package/dist/native/components/ion-tab-bar.js.map +1 -0
- package/dist/native/components/searchable-tabs.d.ts +20 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -0
- package/dist/native/components/searchable-tabs.js +413 -0
- package/dist/native/components/searchable-tabs.js.map +1 -0
- package/dist/native/definitions.d.ts +255 -0
- package/dist/native/definitions.d.ts.map +1 -0
- package/dist/native/definitions.js +0 -0
- package/dist/native/extensions.d.ts +31 -0
- package/dist/native/extensions.d.ts.map +1 -0
- package/dist/native/extensions.js +122 -0
- package/dist/native/extensions.js.map +1 -0
- package/dist/native/foldable/fold-state.d.ts +5 -0
- package/dist/native/foldable/fold-state.d.ts.map +1 -0
- package/dist/native/foldable/fold-state.js +15 -0
- package/dist/native/foldable/fold-state.js.map +1 -0
- package/dist/native/foldable/types.d.ts +12 -0
- package/dist/native/foldable/types.d.ts.map +1 -0
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/html.d.ts +9 -0
- package/dist/native/html.d.ts.map +1 -0
- package/dist/native/html.js +188 -0
- package/dist/native/html.js.map +1 -0
- package/dist/native/index.d.ts +10 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +28 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/legacy-layout.d.ts +5 -0
- package/dist/native/legacy-layout.d.ts.map +1 -0
- package/dist/native/legacy-layout.js +27 -0
- package/dist/native/legacy-layout.js.map +1 -0
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +285 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +9 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +53 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +12 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +110 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +11 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +167 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts +8 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +238 -0
- package/dist/native/prehide.js.map +1 -0
- package/dist/native/runtime.d.ts +7 -0
- package/dist/native/runtime.d.ts.map +1 -0
- package/dist/native/runtime.js +788 -0
- package/dist/native/runtime.js.map +1 -0
- package/dist/native/shared/bridge.d.ts +6 -0
- package/dist/native/shared/bridge.d.ts.map +1 -0
- package/dist/native/shared/bridge.js +18 -0
- package/dist/native/shared/bridge.js.map +1 -0
- package/dist/native/shared/candidate.d.ts +25 -0
- package/dist/native/shared/candidate.d.ts.map +1 -0
- package/dist/native/shared/candidate.js +106 -0
- package/dist/native/shared/candidate.js.map +1 -0
- package/dist/native/shared/clipping.d.ts +5 -0
- package/dist/native/shared/clipping.d.ts.map +1 -0
- package/dist/native/shared/clipping.js +26 -0
- package/dist/native/shared/clipping.js.map +1 -0
- package/dist/native/shared/crossfade.d.ts +12 -0
- package/dist/native/shared/crossfade.d.ts.map +1 -0
- package/dist/native/shared/crossfade.js +67 -0
- package/dist/native/shared/crossfade.js.map +1 -0
- package/dist/native/shared/diagnostics.d.ts +4 -0
- package/dist/native/shared/diagnostics.d.ts.map +1 -0
- package/dist/native/shared/diagnostics.js +18 -0
- package/dist/native/shared/diagnostics.js.map +1 -0
- package/dist/native/shared/dom.d.ts +46 -0
- package/dist/native/shared/dom.d.ts.map +1 -0
- package/dist/native/shared/dom.js +180 -0
- package/dist/native/shared/dom.js.map +1 -0
- package/dist/native/shared/events.d.ts +15 -0
- package/dist/native/shared/events.d.ts.map +1 -0
- package/dist/native/shared/events.js +17 -0
- package/dist/native/shared/events.js.map +1 -0
- package/dist/native/shared/icons.d.ts +8 -0
- package/dist/native/shared/icons.d.ts.map +1 -0
- package/dist/native/shared/icons.js +61 -0
- package/dist/native/shared/icons.js.map +1 -0
- package/dist/native/shared/modal.d.ts +10 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +160 -0
- package/dist/native/shared/modal.js.map +1 -0
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/shared/toolbar-options.d.ts +9 -0
- package/dist/native/shared/toolbar-options.d.ts.map +1 -0
- package/dist/native/shared/toolbar-options.js +36 -0
- package/dist/native/shared/toolbar-options.js.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.js +85 -0
- package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
- package/dist/native/startup.d.ts +8 -0
- package/dist/native/startup.d.ts.map +1 -0
- package/dist/native/startup.js +86 -0
- package/dist/native/startup.js.map +1 -0
- package/dist/native/vertical-bars-web-search.d.ts +10 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +90 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts +5 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +421 -0
- package/dist/native/vertical-bars-web.js.map +1 -0
- package/dist/native-integration/index.d.ts +25 -0
- package/dist/native-integration/index.d.ts.map +1 -0
- package/dist/native-integration/index.js +33 -0
- package/dist/native-integration/index.js.map +1 -0
- package/dist/vertical-bars.d.ts +6 -0
- package/dist/vertical-bars.js +5 -0
- package/docs/configuration.md +187 -0
- package/docs/framework-independent.md +82 -0
- package/docs/migration.md +206 -0
- package/docs/native-mini-player.md +47 -0
- package/docs/native-ui-extensions.md +365 -0
- package/docs/native-ui.md +409 -0
- package/docs/setup.md +205 -0
- package/docs/vertical-bars.md +379 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
- package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
- package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
- package/package.json +89 -4
- package/src/core.ts +15 -0
- package/src/native/activation.ts +394 -0
- package/src/native/components/index.ts +45 -0
- package/src/native/components/ion-back-button.ts +22 -0
- package/src/native/components/ion-button.ts +25 -0
- package/src/native/components/ion-buttons.ts +37 -0
- package/src/native/components/ion-fab.ts +151 -0
- package/src/native/components/ion-menu-button.ts +37 -0
- package/src/native/components/ion-segment.ts +27 -0
- package/src/native/components/ion-tab-bar.ts +50 -0
- package/src/native/components/searchable-tabs.ts +499 -0
- package/src/native/definitions.ts +324 -0
- package/src/native/extensions.ts +176 -0
- package/src/native/foldable/fold-state.ts +10 -0
- package/src/native/foldable/types.ts +4 -0
- package/src/native/html.ts +193 -0
- package/src/native/index.ts +41 -0
- package/src/native/legacy-layout.ts +21 -0
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +349 -0
- package/src/native/overlays/dialog-animation.ts +53 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +120 -0
- package/src/native/overlays/styles.ts +181 -0
- package/src/native/prehide.ts +285 -0
- package/src/native/runtime.ts +988 -0
- package/src/native/shared/bridge.ts +18 -0
- package/src/native/shared/candidate.ts +162 -0
- package/src/native/shared/clipping.ts +24 -0
- package/src/native/shared/crossfade.ts +67 -0
- package/src/native/shared/diagnostics.ts +13 -0
- package/src/native/shared/dom.ts +304 -0
- package/src/native/shared/events.ts +14 -0
- package/src/native/shared/icons.ts +57 -0
- package/src/native/shared/modal.ts +157 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/shared/toolbar-options.ts +53 -0
- package/src/native/shared/vertical-bars-geometry.ts +80 -0
- package/src/native/startup.ts +84 -0
- package/src/native/vertical-bars-web-search.ts +86 -0
- package/src/native/vertical-bars-web.ts +498 -0
- package/src/native-integration/index.ts +49 -0
- package/src/vertical-bars.ts +11 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Native UI (Preview)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
For application-specific SwiftUI or Compose content, see [experimental App-local Native UI extensions](native-ui-extensions.md).
|
|
6
|
+
|
|
7
|
+
# Native UI (Preview)
|
|
8
|
+
|
|
9
|
+
Native UI is a **preview**. The earlier Native UI Shell was bundled with theme versions 1.2.0 and 1.2.1; this guide describes the split package and its renamed API. See [migration](migration.md#native-ui-naming-changes). Its API and supported controls may change before stable status. Stable status is planned after the official release of Xcode 27.1.
|
|
10
|
+
|
|
11
|
+
Native UI gives an Ionic app native navigation and action controls around its Web content. The optional Capacitor iOS plugin renders supported fixed Ionic controls with UIKit or SwiftUI and the system's Liquid Glass material. Page content, scrolling, application state and routing remain in Ionic's WebView.
|
|
12
|
+
|
|
13
|
+
Use this page as the supported-control and API reference. For first-time installation and a working example, start with [Ionic setup](setup.md) or [HTML setup](framework-independent.md).
|
|
14
|
+
|
|
15
|
+
## Enable Native UI
|
|
16
|
+
|
|
17
|
+
Already using the iOS theme? Follow [Add Native UI to your theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/add-native-ui.md). For other Web appearances, use [Native UI setup](setup.md).
|
|
18
|
+
|
|
19
|
+
Install the native package as a direct application dependency so Capacitor can discover it:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @rdlabo/capacitor-native-ui
|
|
23
|
+
npx cap sync ios
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Keep your existing Web styles and enable Native UI once at application startup. The iOS 27 theme is optional:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
|
|
30
|
+
|
|
31
|
+
void enableNativeUI();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`enableNativeUI()` also reads the WebView's effective top-left corner radius and applies it to page transitions. To configure only the transition without enabling native controls, call:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { configureNativeTransition } from '@rdlabo/capacitor-native-ui';
|
|
38
|
+
|
|
39
|
+
await configureNativeTransition();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If using the theme’s `iosTransitionAnimation`, keep that setting; it already coordinates native transitions. For Ionic default or custom animations, configure the [transition adapter](migration.md#native-ui-transition-adapter). No per-page registration, component list, native callback, or Swift view controller is required. Run `npx cap sync ios` after installing or updating the package. The package supplies both Swift Package Manager (SPM) and CocoaPods integration. Keep your existing dependency manager when syncing. If you choose to migrate an existing CocoaPods app to SPM, run `npx cap spm-migration-assistant` and link the generated `CapApp-SPM` package to the app target in Xcode. Build with Xcode 26 or later and Capacitor 8; native glass requires iOS 26 or later. Web, Android, SSR and older iOS keep the Web implementation.
|
|
43
|
+
|
|
44
|
+
This is an opt-in feature. The ordinary `@rdlabo/ionic-theme-ios27` theme entry point does not import Capacitor. Native runtime entry points require `@capacitor/core`, including in browser builds. Native sources are still detected and built by Capacitor's sync when `@rdlabo/capacitor-native-ui` is a direct dependency of a Capacitor project, even if the application does not call `enableNativeUI()`.
|
|
45
|
+
|
|
46
|
+
Native appearance follows the applied class, system or always-dark theme CSS. System theme changes are synchronized while the runtime is active.
|
|
47
|
+
|
|
48
|
+
## Toolbar selection and grouping
|
|
49
|
+
|
|
50
|
+
Configure ordinary and vertical toolbars independently:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const shell = await enableNativeUI({
|
|
54
|
+
horizontal: { buttons: false, button: true, backButton: true },
|
|
55
|
+
vertical: { buttons: true, button: true, backButton: true },
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each layout accepts `boolean | ToolbarProjectionOptions`:
|
|
60
|
+
|
|
61
|
+
| Value | Meaning |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `true` or omitted | All three options default to `true`. |
|
|
64
|
+
| `false` | All three options are `false`; local attributes may still opt in. |
|
|
65
|
+
| Object | Set individual options; omitted fields default to `true`. |
|
|
66
|
+
|
|
67
|
+
For toolbar actions that automatically project only in vertical layout:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const shell = await enableNativeUI({ horizontal: false, vertical: true });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This selects by active layout, not by the Duo model, and does not disable tabs,
|
|
74
|
+
segments or FABs. These settings refine automatic Ionic toolbar projection;
|
|
75
|
+
`ionic.auto` remains its master switch. Explicit `data-native-ui` sources are independent.
|
|
76
|
+
|
|
77
|
+
`button` selects ordinary actions whether grouped
|
|
78
|
+
in the source or standalone; `buttons` only enables grouping of selected actions.
|
|
79
|
+
With `buttons: true, button: false`, no ordinary action projects unless explicitly
|
|
80
|
+
selected. `backButton` independently controls back navigation, including back
|
|
81
|
+
buttons inside `ion-buttons`.
|
|
82
|
+
|
|
83
|
+
Restart the runtime to change these startup options.
|
|
84
|
+
|
|
85
|
+
Use `data-horizontal-bars="true|false"` for local horizontal overrides, and
|
|
86
|
+
`data-vertical-bars="true|false"` for vertical overrides. Each attribute applies
|
|
87
|
+
only in its corresponding layout. On a button/back button it controls selection;
|
|
88
|
+
on `ion-buttons` it controls grouping only. Neither attribute is inherited.
|
|
89
|
+
Invalid or empty values fall back to the matching startup option.
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<ion-buttons data-horizontal-bars="false" data-vertical-bars="true">
|
|
93
|
+
<ion-button fill="clear">First</ion-button>
|
|
94
|
+
<ion-button fill="clear">Second</ion-button>
|
|
95
|
+
</ion-buttons>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The horizontal actions above project individually when otherwise eligible.
|
|
99
|
+
The vertical attribute allows grouping but does not select the text-only children;
|
|
100
|
+
add `data-vertical-bars="true"` to each button to select them explicitly.
|
|
101
|
+
|
|
102
|
+
These settings do not remove CSS or change the source layout. If the app removes
|
|
103
|
+
the source group's glass styling, `horizontal.buttons: false` also prevents a
|
|
104
|
+
shared native group surface. Existing component-mode, fixed-toolbar,
|
|
105
|
+
fill/color and shell opt-out requirements still apply. A horizontal clear button
|
|
106
|
+
inside `ion-buttons` can project individually when grouping is disabled or the
|
|
107
|
+
whole group does not qualify. Back buttons never become ordinary group members.
|
|
108
|
+
Horizontal local overrides are re-evaluated live; vertical placement retains its
|
|
109
|
+
per-page-visit rules. `data-projection="source|system"` remains a separate native
|
|
110
|
+
vertical appearance setting.
|
|
111
|
+
|
|
112
|
+
`enableVerticalControlArea()` remains vertical-only. Use `enableNativeUI()`
|
|
113
|
+
when horizontal projection is needed, and run only one of the two runtimes.
|
|
114
|
+
|
|
115
|
+
## Supported markup
|
|
116
|
+
|
|
117
|
+
The table below describes the ordinary Native UI. Vertical Bars uses the separate [toolbar action rules](vertical-bars.md#toolbar-actions).
|
|
118
|
+
|
|
119
|
+
| Ionic component | Supported appearance and placement | Native rendering |
|
|
120
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
121
|
+
| `ion-button` | `fill="default"`, or clear inside an ungrouped `ion-buttons`, in a fixed header/footer toolbar | Glass `UIButton` |
|
|
122
|
+
| `ion-buttons` | Fixed toolbar, two or more direct clear `ion-button` / `ion-menu-button` children sharing the theme glass | One `UIGlassEffect` surface with independent native buttons |
|
|
123
|
+
| `ion-back-button` | Standard icon and color in a fixed header/footer toolbar | Glass `UIButton`, using the resolved Ionic label/icon |
|
|
124
|
+
| `ion-menu-button` | Fixed toolbar, inside the theme glass `ion-buttons` | Glass `UIButton`; original Ionic menu toggle |
|
|
125
|
+
| `ion-tab-bar` | Fixed tabs, icon-only/label-only items, one icon per item, dot/text badges, selection and disabled state | `UITabBar` and `UITabBarItem` |
|
|
126
|
+
| `ion-segment` | Fixed toolbar, non-scrollable, text **or** one icon per item | `UISegmentedControl` |
|
|
127
|
+
| `ion-fab` / `ion-fab-button` / `ion-fab-list` | Glass FAB in an `ion-content` fixed slot; one main button and optional directional lists | Persistent glass `UIButton` per button; one FAB synchronization group |
|
|
128
|
+
|
|
129
|
+
For the ordinary Native UI, only supported iOS-mode components are eligible. Theme variables are not required; calling `enableNativeUI()` opts into native projection. Explicitly enabled Vertical Bars is mode-independent as described below. `ios-theme-disabled` and the legacy `ios26-disabled` on an element or ancestor always exclude it. A disabled theme on one tab/segment item keeps its whole group on the Web.
|
|
130
|
+
|
|
131
|
+
Use `data-native-ui="disabled"` (or the equivalent `native-ui-disabled` class) to disable only the iOS Native UI while keeping the Web theme. It excludes the element and all its descendants. Adding or removing the attribute or class at runtime automatically restores Web rendering or re-evaluates native eligibility.
|
|
132
|
+
|
|
133
|
+
Only the exact value `disabled` opts out; an empty or unknown value does not opt out. Remove the attribute to restore automatic Ionic projection. For explicitly declared HTML controls, restore the original `data-native-ui="button"` or `data-native-ui="back-button"` value instead. Alternatively, set `data-native-ui="disabled"` on a parent to disable a subtree while preserving its children's declarations. If the `native-ui-disabled` class is also present, remove it as well. This does not disable clicks or change the Web theme. The runtime-owned `data-native-ui-projected` marker is separate; applications should not set it. For the previous opt-out attribute, see [migration](migration.md#native-ui-naming-changes).
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<ion-toolbar data-native-ui="disabled">
|
|
137
|
+
<ion-button>Web glass button</ion-button>
|
|
138
|
+
</ion-toolbar>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If a child inside a shared native surface opts out, the entire surface stays on the Web: this includes button groups, tab bars, segments and FAB lists. Opting out of the search FAB or any part of the search footer disables native search integration; the tab bar can still render natively if it remains eligible.
|
|
142
|
+
|
|
143
|
+
Placement is required even when the appearance is glass. In the ordinary Native UI, buttons, back buttons, menu-button groups and segments need a toolbar directly inside `ion-header` or `ion-footer`, with no `ion-content` ancestor around the control. Buttons directly inside a header/footer, standalone toolbars, and toolbars or headers nested in scrolling content stay on the Web. FABs without `slot="fixed"` also stay on the Web. Moving a projected control to an excluded location restores its Web rendering; moving it back re-evaluates eligibility. When `.ios-theme-vertical-bars` is enabled, a standard `ion-back-button` can instead be projected to the Vertical Control Area from outside a fixed toolbar, including routed content or a persistent app shell. The application chooses where to enable this mode and which Ionic component mode to use; Vertical Bars projection does not require `ios` mode classes. Collapsed headers, opted-out controls and departed pages are excluded. Full-width foreground modals can participate in Vertical Bars as described below; other overlay surfaces keep their own layout.
|
|
144
|
+
|
|
145
|
+
Native tabs accept equal-width items with Ionic's default `layout="icon-top"`. The native bar uses a local compact horizontal and regular vertical size class to preserve the Web's stacked icon/label arrangement on iPad and in landscape. This does not change the app's size class. Label size and weight follow the Web snapshot. Other explicit Ionic layouts (`icon-start`, `icon-end`, `icon-bottom`, `icon-hide`, `label-hide`) and unequal item widths keep the entire tab bar on the Web. Start, center and end placement follow the original `ion-tab-bar`, including RTL. Directional `ion-icon` artwork preserves its rendered RTL flip.
|
|
146
|
+
|
|
147
|
+
Outside Vertical Bars, standalone clear, solid and outline buttons are excluded. A glass `ion-buttons` group of two or more clear buttons is projected as one surface; its children retain separate actions. Menu buttons can also share that group. A single menu button uses its parent `ion-buttons` as the glass surface, so no Web glass is left underneath the native button. A menu button outside this theme glass remains on the Web. Mixed fills, unsupported children, or a theme-disabled child keep the group on the Web. Single clear buttons remain on the Web. Custom button colors, custom back icons/colors, collapsing headers, toolbars inside scrolling content, modal content, scrollable/expanded segments and segment-view integration remain on the Web. Complex slots and unsupported SVG features also fall back to Web. The plugin does not translate arbitrary application CSS into UIKit styles.
|
|
148
|
+
|
|
149
|
+
Native glass samples the Web content actually drawn behind it. Existing toolbar backgrounds and header blur still affect that content. For content to scroll beneath a header, use Ionic's normal translucent-header/fullscreen-content layout; the plugin does not move page content or override an application-owned opaque toolbar background.
|
|
150
|
+
|
|
151
|
+
While a Web input owns the software keyboard, ordinary controls return to Web rendering and are re-evaluated when it closes. This also covers iPad layouts where the keyboard does not move the visual viewport. A native search field keeps its own native keyboard and search surface.
|
|
152
|
+
|
|
153
|
+
When an existing control becomes unsupported, its Web source is painted before its native cover is removed. This avoids a blank handoff, but Web/UIKit updates are not atomic and can briefly overlap. This boundary behavior differs from ordinary page navigation, where unchanged shared native tabs are retained.
|
|
154
|
+
|
|
155
|
+
## State and events
|
|
156
|
+
|
|
157
|
+
Menu buttons use Ionic's resolved default/configured icon or a supported slot icon/label. Native activation clicks the original `ion-menu-button`, preserving `menu` targeting. Non-default menu button types (`submit` / `reset`) remain on the Web. `disabled`, `autoHide`, menu availability and split-pane visibility follow the actual DOM. Opening a menu restores Web controls and retires their native covers; closing it reprojects eligible controls.
|
|
158
|
+
|
|
159
|
+
The DOM owns labels, SVG content, placement, selected values and application behavior. The placement reference is `ion-tab-bar` itself. The native tab content uses that rectangle as its sizing proposal and placement anchor, accounting for UITabBar's larger outer frame: `tab-bar-position-start`, `tab-bar-position-center`, and `tab-bar-position-end` control the horizontal anchor (including RTL), while `slot="bottom"` retains the bottom edge and `slot="top"` retains the top edge. Class changes are reconciled automatically. UIKit owns internal margins and platter sizing, while the supported stacked layout and label typography follow the Web. UIKit can cap the content width even with fill positioning. On viewports at least 768px wide, the Web theme caps standard two-, three-, four- and five-item bars near the iPad stacked platter measurements (188, 274, 336 and 414pt). Smaller viewports retain the phone sizing. The standard bar is 62pt high, with a 54pt selection and a 4pt inset. Adjacent buttons overlap like UIKit’s controls; icon and label alignment is checked against simulator screenshots. These are default appearance targets, not a guarantee for custom fonts, icons or labels; UIKit still determines its intrinsic width. A different native width or height does not reject projection. Badge and title updates retain native item identities and remeasure the platter at the same placement anchor. The plugin does not stretch icons or modify UIKit's internal controls to force CSS item widths. The plugin measures the view subtree containing the native tab controls without private class names or fixed inset corrections; an unrecognized layout restores Web rendering. The runtime watches structural changes, affected shadow roots, sizing, page lifecycle events and overlays. Ancestor `display: none`, `hidden`, theme classes, component removal and disabled changes are reconciled automatically. Native touch events are checked against the current DOM and revision before clicking the original Ionic element.
|
|
160
|
+
|
|
161
|
+
For forms, retain `ion-button type="submit"` and the form's existing submit handler. An external form is still passed as `[form]="formRef"`. The plugin does not call `form.submit()`, add a second submit path or change Angular form ownership. Segment values retain their original type because the original `ion-segment-button` is clicked; programmatic value changes do not emit a synthetic `ionChange`.
|
|
162
|
+
|
|
163
|
+
Labels use native text. Local static SVGs and resolved `ion-icon` SVGs (including `name` and changes to `name`) are rasterized at the display scale and cached. Source projection preserves their colors; system projection of vertical toolbar buttons uses template icons tinted by SwiftUI. Tab SVGs that follow text color use native template rendering, so icons and labels change selection color together without waiting for another bridge image. Multicolor tab artwork retains its original colors. External references, `<use>`, animation, embedded HTML/images, SVG text and stylesheets are excluded. Web fonts and arbitrary slot layouts are not reproduced exactly.
|
|
164
|
+
|
|
165
|
+
The native host accepts input only within native controls. Tab interaction and accessibility are provided by the standard [UITabBar](https://developer.apple.com/documentation/uikit/uitabbar) control. Empty areas pass touches to the WebView. Ionic iOS hides empty badges by default. An empty visible `ion-badge` becomes a native notification dot; a non-empty badge displays its text. Badge background and text colors come from computed DOM styles, including Ionic `color` palettes. Hidden or removed badges clear the native badge. Tabs use UIKit’s standard tab control for selection, touch behavior, badges and accessibility, retaining item identity across selection updates. UIKit owns the item layout within the measured bar, so arbitrary CSS item placement is not reproduced. Native controls expose names, disabled/selected traits and badges to accessibility; the source is hidden from Web accessibility while projected. This is not a guarantee of identical VoiceOver traversal between Web and UIKit.
|
|
166
|
+
|
|
167
|
+
## Searchable tabs
|
|
168
|
+
|
|
169
|
+
Existing `attachTabBarSearchable(tabBar, fabButton, footer)` registrations automatically use native search when their bottom tab bar and glass search controls are supported. No new component option, route, native setup or page listener is required. Ordinary tab bars continue using `UITabBar`; horizontal searchable groups use a persistent `UITabBarController`, `UITab` / `UISearchTab`, and `UISearchController`. The original Capacitor WebView continues to render results and handle navigation.
|
|
170
|
+
|
|
171
|
+
With vertical bars, the native rail keeps every tab and its selection. SwiftUI’s `searchable` and `searchToolbarBehavior(.minimize)` provide the system search button, field, dismissal control and transitions within the same native navigation surface. Apple determines their placement for the current device layout and focuses the native field when system search opens. Input, focus, clear and submit events use the same bridge as horizontal search. As soon as vertical bars are enabled, the Web projection places the registered search button above visible vertical tabs, or at the bottom of the rail when the tabs are hidden, and forwards activation to the original FAB. Native search replaces that Web projection when available. The Web search presentation also preserves vertical tabs.
|
|
172
|
+
|
|
173
|
+
Search registration does not bypass placement restrictions. Its searchbar and close button must be in fixed footer toolbars. Its trigger must belong to an `ion-fab[slot="fixed"]` directly inside `ion-content`, or directly on the existing non-scrolling `.ion-page` layout. A wrapper inside scrolling content is not a fixed slot.
|
|
174
|
+
|
|
175
|
+
For horizontal tabs, once a supported search layout has been measured, Native UI keeps the searchable controller while the registration is alive, including when the page is transitioning or temporarily unavailable (`available: false`). A first registration on a hidden page keeps ordinary tabs until the search controls can be measured. The same controller manages resting tabs and active search. When resting, its view is fitted to the source `ion-tab-bar` to preserve the ordinary tab platter’s source width. UIKit can limit the platter width for small tab groups. UIKit owns the search button's placement beside the tabs. If UIKit cannot fit a horizontal search group to the source layout, search stays on the Web. Eligible tabs remain native unless they would cover the Web search trigger; overlapping tabs also stay on the Web. Register before the destination page finishes entering (for example in `ionViewWillEnter`); projection begins once its search controls can be measured.
|
|
176
|
+
|
|
177
|
+
Opening search preserves the selected Ionic tab and does not automatically show the keyboard (`automaticallyActivatesSearch` stays off). Tap the field or call `ion-searchbar.setFocus()` for the keyboard. While search is active, Native UI freezes Web layout projection and holds Capacitor Keyboard resize at `none`; UIKit owns tab/search chrome without frame fitting during the session. Closing search restores the ordinary tabs to their source placement. Ordinary native tabs keep an optimistic selection until the Web `selected` state catches up. Input events and application `value` updates continue across the bridge until search closes. The trigger's resolved SVG, accessibility label and the search icon are projected from Ionic, including `ion-icon name`.
|
|
178
|
+
|
|
179
|
+
Native edits pass through Ionic's input handlers, preserving `ionInput` debounce, `ionChange`, `ionFocus`, `ionBlur`, and `ionClear`. Programmatic `value` changes do not emit `ionInput`; synchronous application corrections and stale native input are distinguished. Native editing owns marked text and the caret. Returning through the footer's close action retains the value and does not emit `ionCancel` or `ionClear`.
|
|
180
|
+
|
|
181
|
+
The first supported search configuration uses iOS-mode glass searchbars with the standard search keyboard, default clear control, no internal cancel button, and default autocorrection/capitalization settings. Custom input modes, return-key hints, min/max length, autocomplete, autocorrection, spellcheck, clear icons and classic searchbars remain on the Web. `disabled`, `placeholder`, `value` and `setFocus()` are synchronized for supported groups. General standalone searchbars are outside this feature.
|
|
182
|
+
|
|
183
|
+
Page retirement, overlays, theme exclusion and lost native ownership close the native presentation and preserve the latest synchronized/application value. A later search starts from that closed state. The registration survives cached page transitions; reattaching on every return is not required. Bridge waits are finite. When an opening request loses the bridge, its pending Enter can complete through the existing Web animation. A disconnected bridge cannot recover native characters that were never delivered to JavaScript.
|
|
184
|
+
|
|
185
|
+
Replacing the registered `ion-searchbar` or its input retires the old editing session. The replacement keeps its own application value and starts a new native session when reopened.
|
|
186
|
+
|
|
187
|
+
While enabled, Native UI suppresses the WebView’s top scroll-edge effect because Ionic already paints the header edge. This prevents a second dark gradient when the OS and Web themes differ. The original setting is restored on destroy.
|
|
188
|
+
|
|
189
|
+
The native search controller remains visible over its own keyboard. Other projected controls are hidden while a Web input opens the keyboard. UIKit's own accessibility and Reduce Motion behavior apply to the standard controls; full VoiceOver traversal is not a verified parity guarantee.
|
|
190
|
+
|
|
191
|
+
## Transitions and recovery
|
|
192
|
+
|
|
193
|
+
FABs retain Ionic's `activated`, per-child `show`, `close()` and original click handlers. Multiple lists, initial expansion, small buttons and `edge` use each button's measured layout. The native side reflects Ionic's staggered visibility without adding another timer or open/close controller. Main-button icon changes crossfade using the resolved `closeIcon`; Reduce Motion disables this crossfade. Native FAB button instances persist across open/close updates. The source FAB stays projected throughout ordinary opening and closing.
|
|
194
|
+
|
|
195
|
+
FAB support covers the standard circular glass appearance, text and resolved static SVG/`ion-icon` content, including RTL icon mirroring. One unsupported child keeps the whole FAB in Web rendering, including when its list is closed. Colored solid buttons, submit/reset or href FABs, custom host backgrounds/shapes/motion, non-fixed-slot placement and unsupported artwork remain Web. For example, the demo's red-background `floating-action-button-fixed` page retains its existing Web appearance. A fixed-slot FAB must be a direct child of `ion-content`; a `slot="fixed"` attribute on a page sibling is not a content slot.
|
|
196
|
+
|
|
197
|
+
Custom host animation or transition declarations on the FAB, list or button keep the group on the Web until removed. Button transforms support the standard identity and hidden-child scale(0), not custom scaling. For children inside a `display:none` list, browsers can report a computed transform of `none` even when a custom transform is declared. Such transforms are checked when layout becomes available; the whole FAB then returns to Web if needed. The plugin does not parse application stylesheets or temporarily open lists to predict hidden layout.
|
|
198
|
+
|
|
199
|
+
The built-in `iosTransitionAnimation` waits for native retirement before starting the Web animation. Interactive progress and completion/cancellation are queued while that retirement is in progress. Stationary shared tabs are retained. First render and transitions without an animation builder are covered by the startup runtime and Ionic lifecycle events. Default Ionic and custom navigation builders can use the same integration through `withNativeUITransition()`, exported by `@rdlabo/ionic-theme-ios27`. See [Connect your navigation animation](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo-with-original-theme.md#3-connect-your-navigation-animation) for the setup.
|
|
200
|
+
|
|
201
|
+
Ordinary native/Web handoffs share a 180ms opacity crossfade, including Vertical Bars in both directions. Outgoing visuals cannot receive input or accessibility focus. Reduce Motion and modal presentation use immediate handoffs; search controllers keep their UIKit-managed transition.
|
|
202
|
+
|
|
203
|
+
Tab switches skip the Web/native crossfade so a retiring UIKit snapshot cannot linger over the next tab. Detection uses the router URL versus the still-selected tab on `ionViewWillLeave` (and vanilla `ionTabsWillChange` / `ionTabsDidChange` DOM events). Stack pushes and pops keep the normal 180ms handoff.
|
|
204
|
+
|
|
205
|
+
Standard Ionic overlays suspend ordinary native projection until dismissal. Vertical Bars use a separate policy:
|
|
206
|
+
|
|
207
|
+
| Surface or condition | Vertical Bars behavior |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| Web toast, ordinary popover or legacy picker overlay | Keep the native rail visible, updated and interactive. |
|
|
210
|
+
| Popover opened from a Vertical Bars button, rendered on Web | Return the rail to Web projection until dismissal, including native rejection or preparation failure. |
|
|
211
|
+
| Alert, action sheet or loading | Restore Web rendering so the overlay and its dimming backdrop cover the controls correctly. |
|
|
212
|
+
| Menu | Keep the existing native rail behavior. |
|
|
213
|
+
| Full-width foreground modal | Project the modal's controls instead of the covered page's controls. |
|
|
214
|
+
| Modal that cannot use Vertical Bars | Restore Web rendering. |
|
|
215
|
+
| Explicit suspension, unavailable native layout, hidden document or unsupported keyboard/viewport geometry | Preserve the existing fallback behavior. |
|
|
216
|
+
|
|
217
|
+
Native projection resumes only after every overlay requiring Web rendering closes. The opt-in native Vertical Bars popover described below keeps its own SwiftUI presentation and input handling. Unsupported searchable-tab configurations use the existing Web animation and share this ownership with Web glass gestures. CSS motion of supported containing surfaces also causes temporary Web rendering.
|
|
218
|
+
|
|
219
|
+
During retirement, the source is restored and allowed to paint before its native cover is removed. During acquisition, the source is hidden only after a successful, current native response. Delayed responses are revalidated per control: existing eligible controls retain their native cover while content updates catch up. Only removed or ineligible sources return to Web; newly acquired sources require an exact acknowledgement. Ordinary page mutations never call the global clear operation. UIKit tab instances and items are retained, and equal frames/selections are not reapplied. Duplicate/stale activations are discarded. WebKit and UIKit still render separately: the implementation avoids an intentional blank frame, but does not provide an OS-level atomic compositing guarantee. Validate custom transitions and overlays on the app's supported simulators before rollout; unknown overlay systems are outside the automatic integration contract.
|
|
220
|
+
|
|
221
|
+
If a bridge update fails or times out, the runtime stops and restores Web rendering. It does not automatically reconnect. `getStatus()` reports `stopped` and the reason; to retry deliberately, call `destroy()` on that handle and then `enableNativeUI()` again.
|
|
222
|
+
|
|
223
|
+
Failures or timeouts in required startup bridge calls return a Web fallback handle. Optional WebView metrics may fail without disabling native controls. Both `destroy()` and `enableNativeUI({ enabled: false })` wait for teardown; repeated calls share its completion. A new activation requested while teardown is running waits for it, including when the application calls `destroy()` without awaiting it. Await these promises when the next application operation depends on cleanup, and handle any rejection.
|
|
224
|
+
|
|
225
|
+
For diagnostics or application teardown:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const shell = await enableNativeUI(); // repeated calls share the runtime
|
|
229
|
+
console.log(shell.getStatus()); // state, projected control count, update count, failure reason
|
|
230
|
+
await shell.destroy(); // restore DOM, remove native controls and release listeners/cache
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Projection is enabled by calling the startup API. Partial settings override defaults:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const shell = await enableNativeUI({ ionic: { auto: { tabs: false } } }); // automatic Ionic projection except tabs
|
|
237
|
+
await shell.disableVerticalControlArea(); // retain ordinary native controls
|
|
238
|
+
await shell.enableVerticalControlArea();
|
|
239
|
+
await shell.destroy(); // stop all projection
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
See [all configuration options](configuration.md) for defaults, rail-only usage and lifecycle methods. For explicit HTML sources without Ionic, see [framework-independent setup](framework-independent.md).
|
|
243
|
+
|
|
244
|
+
### Popovers anchored to Vertical Bars
|
|
245
|
+
|
|
246
|
+
**Native popover presentation is Vertical Bars only.** It requires an accepted native Vertical Bars `ion-button` anchor; enabling `verticalBars.popover` does not make ordinary toolbar or body popovers native. This feature also works with the standalone Vertical Bars runtime, without enabling the full Native UI.
|
|
247
|
+
|
|
248
|
+
Enable `verticalBars.popover` to present popovers from accepted native Vertical Bars `ion-button` controls. Automatic Ionic button projection is enabled by default; when `ionic.auto` is `false`, mark individual anchors with `data-native-ui="button"`:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
await enableNativeUI({
|
|
252
|
+
verticalBars: { popover: true },
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The same option is available through `enableVerticalControlArea()`. Use an `ion-popover` whose `trigger` identifies the projected button, or pass that button's click event to `PopoverController.create({ event, ... })`. The button must be accepted by native Vertical Bars; regular toolbar buttons, body controls, and missing or rejected anchors retain Ionic's Web presentation.
|
|
257
|
+
|
|
258
|
+
When Vertical Bars return to Web rendering, the runtime measures the current native back button, toolbar actions and tab surfaces before retiring them. Web fallback uses those rectangles and paints before native removal. Measurements are tied to the source elements and viewport; unavailable measurements use the CSS layout. This aligns placement and size, but the Web glass and icon rendering can still differ from system materials.
|
|
259
|
+
|
|
260
|
+
The page's native controls stay projected throughout preparation, presentation and dismissal. If the same native anchor accepts a tap while Ionic is still dismissing, one activation is retained and revalidated after cleanup; repeated taps coalesce. The relay prepares its child WebView while Ionic initializes the content, completes the Web enter animation without motion behind the hidden source host, and presents through the button's standard SwiftUI popover. It does not resize or reparent the source WebView. Ionic retains its controller, lifecycle, live application handlers and dismissal data; the live content temporarily moves into the child document and is restored during cleanup.
|
|
261
|
+
|
|
262
|
+
Only one popover is relayed at a time. A nested popover or modal created in the child document is moved as a live element back to the source document before the child closes, including overlays appended outside the relayed content. It continues as a Web overlay with its handlers and dismissal data; its presentation event is not replayed. Opening another overlay or menu, leaving the page, suspending the shell, or losing the native layout restores Web rendering. Modal, sheet and alert relay are outside this feature.
|
|
263
|
+
|
|
264
|
+
Native presentation owns the arrow, corners and animation; custom Web enter/leave animations do not replace them. The source enter builder runs once with zero duration and delay (including child animations), using the native anchor rectangle for the default `reference="trigger"` layout and visible state for Web fallback. Explicit `reference="event"` retains Ionic’s event-coordinate semantics. An explicit `enterAnimation` is honored; otherwise this package’s iOS popover builder is used. Set `enterAnimation` on the overlay to use a custom builder rather than relying on global `popoverEnter` configuration. Material Design (`md`) popovers keep Ionic’s Web presentation. Initial content dimensions come from Ionic.
|
|
265
|
+
|
|
266
|
+
The relay copies document and popover shadow styles and preserves the content border spacing. Before native presentation it compares selected computed layout and appearance properties at the source dimensions. Detected differences, unsupported ancestor visual effects or size containers, and content exceeding 1,000 elements restore the live Web popover. Changes to mirrored ancestor attributes or source head styles also restore Web rendering. This is a conservative compatibility check, not a guarantee for arbitrary CSS (including pseudo-elements and dynamic CSSOM changes), document-bound queries, or full VoiceOver/hardware-keyboard parity. Failure to prepare or find the native anchor restores the Web popover. A popover opened from a Vertical Bars button also returns the rail to Web projection for the remainder of that presentation; dismissal restores native projection.
|
|
267
|
+
|
|
268
|
+
For a custom modal or overlay that Native UI cannot detect, acquire a suspension before presenting it. The resolved suspension means projected controls have returned to Web rendering. Always release it after dismissal:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const suspension = await shell.suspend();
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await modal.present();
|
|
275
|
+
await modal.onDidDismiss();
|
|
276
|
+
} finally {
|
|
277
|
+
await suspension.resume();
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Suspensions are nestable and `resume()` is idempotent. Native projection resumes only after every active suspension has been released, using the current DOM rather than a stale snapshot.
|
|
282
|
+
|
|
283
|
+
The native material and control appearance follow the running iOS version; an iOS 26 device does not acquire iOS 27's appearance merely by installing this theme.
|
|
284
|
+
|
|
285
|
+
## Support iPhone Duo (preview)
|
|
286
|
+
|
|
287
|
+
iPhone Duo support, including standalone vertical bars without Native UI, is available as a **preview** in `1.2.0` alongside Native UI. Its APIs and supported behavior may change.
|
|
288
|
+
|
|
289
|
+
The standalone Vertical Control Area entry point (`@rdlabo/capacitor-native-ui/vertical-bars`) works with application-supplied Web layout. The theme package offers an [optional rail stylesheet](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/vertical-bars-layout.md), `@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css`, which can be used without loading the full iOS 27 appearance. Call `enableNativeUI({ verticalBars: { only: true } })` for this use case; it projects only controls placed in the vertical area. Apps already calling `enableNativeUI()` should keep that single runtime rather than starting both. See [iPhone Duo support](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo.md) for the complete setup, including hinge posture and the split-pane layout for apps that do not use this shell at all.
|
|
290
|
+
|
|
291
|
+
Vertical Bars uses a SwiftUI `TabView` and toolbar in the system rail. Ionic remains the source of labels, icons, selected/disabled state and actions. See [Vertical Bars](vertical-bars.md) for rail layout, native fallback and supported overlays.
|
|
292
|
+
|
|
293
|
+
In Vertical Bars, automatic selection of fixed-toolbar `ion-button` actions requires an `ion-icon` or SVG with `slot="icon-only"`; `data-vertical-bars="true"` can explicitly select other supported buttons. All fills and Ionic colors are eligible; submit buttons follow the same placement rule. Add `.ios-theme-horizontal-only` to a button or its `ion-buttons` group to keep it horizontal. See [Toolbar actions](vertical-bars.md#toolbar-actions) for placement and exclusion rules.
|
|
294
|
+
|
|
295
|
+
**Native button appearance:** `1.2.0` defaults native vertical buttons to `buttonProjection: 'system'`. Use `source` to project Ionic fill and colors, with `data-projection="source|system"` or the equivalent `ios-theme-projection-source` / `ios-theme-projection-system` classes for local exceptions. These settings affect native vertical `ion-button` and `ion-menu-button` actions only. Horizontal controls and Web clones keep their existing behavior. See [Choose button appearance](vertical-bars.md#choose-button-appearance) for migration, priority and fill rules.
|
|
296
|
+
|
|
297
|
+
For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo-with-original-theme.md).
|
|
298
|
+
|
|
299
|
+
## Native UI API
|
|
300
|
+
|
|
301
|
+
The generated reference below documents the handle returned by `enableNativeUI()`. The underlying Capacitor bridge and its control-snapshot protocol are implementation details.
|
|
302
|
+
|
|
303
|
+
<docgen-index>
|
|
304
|
+
|
|
305
|
+
* [`enableVerticalControlArea()`](#enableverticalcontrolarea)
|
|
306
|
+
* [`disableVerticalControlArea()`](#disableverticalcontrolarea)
|
|
307
|
+
* [`markForCheck()`](#markforcheck)
|
|
308
|
+
* [`getStatus()`](#getstatus)
|
|
309
|
+
* [`suspend()`](#suspend)
|
|
310
|
+
* [`destroy()`](#destroy)
|
|
311
|
+
* [Interfaces](#interfaces)
|
|
312
|
+
|
|
313
|
+
</docgen-index>
|
|
314
|
+
|
|
315
|
+
<docgen-api>
|
|
316
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
317
|
+
|
|
318
|
+
A running shell. Placement changes preserve all other startup settings.
|
|
319
|
+
|
|
320
|
+
### enableVerticalControlArea()
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
enableVerticalControlArea() => Promise<void>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Enable rail projection when available, preserving other settings. Rejects after destroy.
|
|
327
|
+
|
|
328
|
+
--------------------
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
### disableVerticalControlArea()
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
disableVerticalControlArea() => Promise<void>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Restore rail sources to the Web without stopping ordinary native controls. Rejects after destroy.
|
|
338
|
+
|
|
339
|
+
--------------------
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
### markForCheck()
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
markForCheck() => void
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Schedule a source recheck. Calls coalesce; unchanged snapshots are not sent. No-op after destroy or without a runtime.
|
|
349
|
+
|
|
350
|
+
--------------------
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
### getStatus()
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
getStatus() => NativeUIStatus
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Returns the current Web/native projection state.
|
|
360
|
+
|
|
361
|
+
**Returns:** <code><a href="#nativeuistatus">NativeUIStatus</a></code>
|
|
362
|
+
|
|
363
|
+
--------------------
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
### suspend()
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
suspend() => Promise<NativeUISuspension>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Restores projected controls to the Web until the returned lease is resumed.
|
|
373
|
+
|
|
374
|
+
**Returns:** <code>Promise<<a href="#nativeuisuspension">NativeUISuspension</a>></code>
|
|
375
|
+
|
|
376
|
+
--------------------
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
### destroy()
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
destroy() => Promise<void>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Stops synchronization, restores Web controls and releases native resources.
|
|
386
|
+
|
|
387
|
+
--------------------
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
### Interfaces
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
#### NativeUIStatus
|
|
394
|
+
|
|
395
|
+
| Prop | Type |
|
|
396
|
+
| --------------- | ------------------------------------------- |
|
|
397
|
+
| **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
|
|
398
|
+
| **`projected`** | <code>number</code> |
|
|
399
|
+
| **`updates`** | <code>number</code> |
|
|
400
|
+
| **`reason`** | <code>string</code> |
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
#### NativeUISuspension
|
|
404
|
+
|
|
405
|
+
| Method | Signature | Description |
|
|
406
|
+
| ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
407
|
+
| **resume** | () => Promise<void> | Releases this suspension. Native projection resumes after all active suspensions are released. |
|
|
408
|
+
|
|
409
|
+
</docgen-api>
|
package/docs/setup.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Set up Native UI in an Ionic app
|
|
2
|
+
|
|
3
|
+
> This guide targets the upcoming package split. Until the first native package release, use the [candidate archive installation](../README.md#try-the-unreleased-packages) in place of registry install commands.
|
|
4
|
+
|
|
5
|
+
Keep your existing Web appearance and render supported Ionic controls natively on iOS. This guide does not require iOS theme stylesheets.
|
|
6
|
+
|
|
7
|
+
Already using the iOS 27 theme? Follow [Add Native UI to your theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/add-native-ui.md). Without Ionic, follow the [HTML setup](framework-independent.md) instead.
|
|
8
|
+
|
|
9
|
+
## Before you start
|
|
10
|
+
|
|
11
|
+
Use an existing Ionic `>=8.8.1 <10` application with Capacitor 8 and an iOS project. Build with Xcode 26 or later; ordinary native controls require iOS 26 or later. Vertical Bars have separate [device requirements](vertical-bars.md). Keep your current SPM or CocoaPods integration.
|
|
12
|
+
|
|
13
|
+
If the app does not yet use Capacitor, create and configure its Capacitor iOS project before following the native steps below. Browser previews retain Web controls and do not demonstrate UIKit or SwiftUI rendering.
|
|
14
|
+
|
|
15
|
+
## 1. Install the plugin
|
|
16
|
+
|
|
17
|
+
Keep your Capacitor packages on version 8. Add these direct application dependencies:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @rdlabo/capacitor-native-ui
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The Ionic adapter uses `@ionic/core`, which your Ionic app already supplies. Do not remove your existing Web styles: they style page content and fallback controls.
|
|
24
|
+
|
|
25
|
+
## 2. Enable the shell
|
|
26
|
+
|
|
27
|
+
Call once from application startup after `ion-app` is mounted:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
|
|
31
|
+
|
|
32
|
+
const shell = await enableNativeUI();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Keep the returned handle if you need to disable features or stop projection. Call `await shell.destroy()` when its application owner is disposed. Do not start a new shell for each page.
|
|
36
|
+
|
|
37
|
+
Existing Ionic markup supplies labels, icons, disabled state, and actions. For example, an existing fixed page header can contain:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<ion-header>
|
|
41
|
+
<ion-toolbar>
|
|
42
|
+
<ion-title>My app</ion-title>
|
|
43
|
+
<ion-buttons slot="end">
|
|
44
|
+
<ion-button aria-label="Add">
|
|
45
|
+
<ion-icon slot="icon-only" name="add"></ion-icon>
|
|
46
|
+
</ion-button>
|
|
47
|
+
</ion-buttons>
|
|
48
|
+
</ion-toolbar>
|
|
49
|
+
</ion-header>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep your framework's handler on the original button and register the icon as usual. See [supported markup](native-ui.md#supported-markup) for eligibility; unsupported controls stay on the Web.
|
|
53
|
+
|
|
54
|
+
### Place startup in your framework's app root
|
|
55
|
+
|
|
56
|
+
Keep one shell for the whole app, above routed pages. The following app-owned helper serializes asynchronous startup and cleanup, including React development remounts. Put your shared startup options here, and remove any other `enableNativeUI()` call:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// native-ui.ts — place beside your root component
|
|
60
|
+
import { enableNativeUI, type NativeUIHandle } from '@rdlabo/capacitor-native-ui';
|
|
61
|
+
// Without Ionic, use @rdlabo/capacitor-native-ui/core instead.
|
|
62
|
+
|
|
63
|
+
let lifecycle = Promise.resolve();
|
|
64
|
+
const report = (error: unknown) => console.error('Native UI lifecycle failed', error);
|
|
65
|
+
|
|
66
|
+
export function mountNativeUI(): () => void {
|
|
67
|
+
let disposed = false;
|
|
68
|
+
let shell: NativeUIHandle | undefined;
|
|
69
|
+
lifecycle = lifecycle.then(async () => {
|
|
70
|
+
if (disposed) return;
|
|
71
|
+
shell = await enableNativeUI(); // Put your options here.
|
|
72
|
+
if (disposed) await shell.destroy();
|
|
73
|
+
}).catch(report);
|
|
74
|
+
return () => {
|
|
75
|
+
disposed = true;
|
|
76
|
+
lifecycle = lifecycle.then(async () => {
|
|
77
|
+
await shell?.destroy();
|
|
78
|
+
shell = undefined;
|
|
79
|
+
}).catch(report);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Choose **one** framework below. The helper belongs to your application; it is not another package to install.
|
|
85
|
+
|
|
86
|
+
**Angular:** call from the root component after the first DOM render, and register cleanup with its `DestroyRef`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { afterNextRender, DestroyRef, inject } from '@angular/core';
|
|
90
|
+
import { mountNativeUI } from './native-ui';
|
|
91
|
+
|
|
92
|
+
// In the constructor of your root AppComponent:
|
|
93
|
+
const destroyRef = inject(DestroyRef);
|
|
94
|
+
afterNextRender(() => {
|
|
95
|
+
const stop = mountNativeUI();
|
|
96
|
+
destroyRef.onDestroy(stop);
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**React:** use one effect in the root component that renders `IonApp`:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { useEffect } from 'react';
|
|
104
|
+
import { mountNativeUI } from './native-ui';
|
|
105
|
+
|
|
106
|
+
// Inside your root App component:
|
|
107
|
+
useEffect(() => mountNativeUI(), []);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Vue:** register lifecycle hooks in the root component's `<script setup>`:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { onMounted, onBeforeUnmount } from 'vue';
|
|
114
|
+
import { mountNativeUI } from './native-ui';
|
|
115
|
+
|
|
116
|
+
let stop: (() => void) | undefined;
|
|
117
|
+
onMounted(() => { stop = mountNativeUI(); });
|
|
118
|
+
onBeforeUnmount(() => { stop?.(); });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
These hooks manage the app's lifetime, not individual page transitions. Configure navigation below as well.
|
|
122
|
+
|
|
123
|
+
## 3. Coordinate navigation
|
|
124
|
+
|
|
125
|
+
For optional Ionic animation coordination, install `@rdlabo/ionic-theme-ios27` and configure navigation before Ionic initializes. Its adapter can retain Ionic's default transitions without loading theme CSS:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
npm install @rdlabo/ionic-theme-ios27
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { iosTransitionAnimation, mdTransitionAnimation, type AnimationBuilder } from '@ionic/core';
|
|
133
|
+
import { withNativeUITransition } from '@rdlabo/ionic-theme-ios27';
|
|
134
|
+
|
|
135
|
+
const defaultTransition: AnimationBuilder = (baseEl, opts) =>
|
|
136
|
+
(opts.mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation)(baseEl, opts);
|
|
137
|
+
|
|
138
|
+
const ionicConfig = {
|
|
139
|
+
navAnimation: withNativeUITransition(defaultTransition),
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Pass this option to Angular's `provideIonicAngular`, React's `setupIonicReact`, or Vue's `IonicVue` options. For custom animations, follow the [transition adapter instructions](migration.md#native-ui-transition-adapter). This optional adapter requires the theme package but does not require its stylesheets or replace your animation.
|
|
144
|
+
|
|
145
|
+
For a plugin-only app, use [navigation without the theme](#navigation-without-the-theme) below. Configure one approach before testing page navigation.
|
|
146
|
+
|
|
147
|
+
## 4. Build, sync, and run
|
|
148
|
+
|
|
149
|
+
Run your application's Web build, then sync:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npx cap sync ios
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Run the iOS app from Xcode on an iOS 26+ device or simulator. Supported fixed controls should render natively and still invoke their original handlers. Navigate forward and back, including a canceled swipe-back gesture. A browser or Android build should retain working Web controls.
|
|
156
|
+
|
|
157
|
+
| If you see… | Check… |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| All controls remain on the Web on iOS | The native package is a direct dependency, the app was synced and rebuilt, and startup ran on a supported OS. |
|
|
160
|
+
| Only one control remains on the Web | Compare its markup and styles with [supported controls](native-ui.md#supported-markup). |
|
|
161
|
+
| Controls overlap during navigation | Configure the transition adapter or suspend/resume lifecycle before navigating. |
|
|
162
|
+
| Missing styles or spacing | Retain the app's Web CSS; the plugin does not supply page layout. |
|
|
163
|
+
|
|
164
|
+
## Next steps
|
|
165
|
+
|
|
166
|
+
- [Vertical Bars only](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo-with-original-theme.md): a step-by-step rail setup with your existing theme.
|
|
167
|
+
- [Configuration](configuration.md): choose controls and change rail settings at runtime.
|
|
168
|
+
- [Custom SwiftUI or Compose content](native-ui-extensions.md): create an app-local extension.
|
|
169
|
+
- [Migration](migration.md): update older package names, imports, and native projects.
|
|
170
|
+
|
|
171
|
+
## Navigation without the theme
|
|
172
|
+
|
|
173
|
+
Native UI does not depend on a theme or an Ionic animation builder. Your router can suspend projection before changing pages and resume it after completion or cancellation:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const suspension = await shell.suspend();
|
|
177
|
+
try {
|
|
178
|
+
await navigate();
|
|
179
|
+
} finally {
|
|
180
|
+
await suspension.resume();
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The promise must finish after the DOM and any page animation have settled, not merely after the URL changes. Connect browser back, gesture cancellation, and programmatic navigation to the same lifecycle. For Ionic, use the transition adapter above to cover animated and interactive transitions.
|
|
185
|
+
|
|
186
|
+
For a routerless HTML app, this is a complete animated page switch. A canceled animation restores the old page before resuming projection:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
async function showPage(current: HTMLElement, next: HTMLElement) {
|
|
190
|
+
const lease = await shell.suspend();
|
|
191
|
+
try {
|
|
192
|
+
current.hidden = true;
|
|
193
|
+
next.hidden = false;
|
|
194
|
+
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
195
|
+
const animation = next.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150 });
|
|
196
|
+
try { await animation.finished; }
|
|
197
|
+
catch { next.hidden = true; current.hidden = false; }
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
await lease.resume();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Serialize page switches if your app allows another navigation before this promise settles. Keep the source click handlers responsible for your router or history; the plugin does not navigate for you.
|