@rdlabo/capacitor-native-ui 0.0.0-stage → 2.0.0-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.
Files changed (261) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +23 -0
  3. package/README.md +66 -2
  4. package/RdlaboCapacitorNativeUi.podspec +17 -0
  5. package/android/build.gradle +69 -0
  6. package/android/proguard-rules.pro +0 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
  9. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
  10. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
  11. package/dist/_virtual/_rolldown/runtime.js +13 -0
  12. package/dist/core.d.ts +4 -0
  13. package/dist/core.js +3 -0
  14. package/dist/native/activation.d.ts +14 -0
  15. package/dist/native/activation.d.ts.map +1 -0
  16. package/dist/native/activation.js +305 -0
  17. package/dist/native/activation.js.map +1 -0
  18. package/dist/native/components/index.d.ts +19 -0
  19. package/dist/native/components/index.d.ts.map +1 -0
  20. package/dist/native/components/index.js +38 -0
  21. package/dist/native/components/index.js.map +1 -0
  22. package/dist/native/components/ion-back-button.d.ts +10 -0
  23. package/dist/native/components/ion-back-button.d.ts.map +1 -0
  24. package/dist/native/components/ion-back-button.js +21 -0
  25. package/dist/native/components/ion-back-button.js.map +1 -0
  26. package/dist/native/components/ion-button.d.ts +10 -0
  27. package/dist/native/components/ion-button.d.ts.map +1 -0
  28. package/dist/native/components/ion-button.js +25 -0
  29. package/dist/native/components/ion-button.js.map +1 -0
  30. package/dist/native/components/ion-buttons.d.ts +11 -0
  31. package/dist/native/components/ion-buttons.d.ts.map +1 -0
  32. package/dist/native/components/ion-buttons.js +30 -0
  33. package/dist/native/components/ion-buttons.js.map +1 -0
  34. package/dist/native/components/ion-fab.d.ts +10 -0
  35. package/dist/native/components/ion-fab.d.ts.map +1 -0
  36. package/dist/native/components/ion-fab.js +132 -0
  37. package/dist/native/components/ion-fab.js.map +1 -0
  38. package/dist/native/components/ion-menu-button.d.ts +13 -0
  39. package/dist/native/components/ion-menu-button.d.ts.map +1 -0
  40. package/dist/native/components/ion-menu-button.js +44 -0
  41. package/dist/native/components/ion-menu-button.js.map +1 -0
  42. package/dist/native/components/ion-segment.d.ts +11 -0
  43. package/dist/native/components/ion-segment.d.ts.map +1 -0
  44. package/dist/native/components/ion-segment.js +30 -0
  45. package/dist/native/components/ion-segment.js.map +1 -0
  46. package/dist/native/components/ion-tab-bar.d.ts +11 -0
  47. package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
  48. package/dist/native/components/ion-tab-bar.js +50 -0
  49. package/dist/native/components/ion-tab-bar.js.map +1 -0
  50. package/dist/native/components/searchable-tabs.d.ts +20 -0
  51. package/dist/native/components/searchable-tabs.d.ts.map +1 -0
  52. package/dist/native/components/searchable-tabs.js +413 -0
  53. package/dist/native/components/searchable-tabs.js.map +1 -0
  54. package/dist/native/definitions.d.ts +237 -0
  55. package/dist/native/definitions.d.ts.map +1 -0
  56. package/dist/native/definitions.js +0 -0
  57. package/dist/native/extensions.d.ts +31 -0
  58. package/dist/native/extensions.d.ts.map +1 -0
  59. package/dist/native/extensions.js +122 -0
  60. package/dist/native/extensions.js.map +1 -0
  61. package/dist/native/foldable/fold-state.d.ts +5 -0
  62. package/dist/native/foldable/fold-state.d.ts.map +1 -0
  63. package/dist/native/foldable/fold-state.js +15 -0
  64. package/dist/native/foldable/fold-state.js.map +1 -0
  65. package/dist/native/foldable/types.d.ts +12 -0
  66. package/dist/native/foldable/types.d.ts.map +1 -0
  67. package/dist/native/foldable/types.js +0 -0
  68. package/dist/native/html.d.ts +9 -0
  69. package/dist/native/html.d.ts.map +1 -0
  70. package/dist/native/html.js +197 -0
  71. package/dist/native/html.js.map +1 -0
  72. package/dist/native/index.d.ts +10 -0
  73. package/dist/native/index.d.ts.map +1 -0
  74. package/dist/native/index.js +19 -0
  75. package/dist/native/index.js.map +1 -0
  76. package/dist/native/legacy-layout.d.ts +5 -0
  77. package/dist/native/legacy-layout.d.ts.map +1 -0
  78. package/dist/native/legacy-layout.js +27 -0
  79. package/dist/native/legacy-layout.js.map +1 -0
  80. package/dist/native/overlays/content.d.ts +4 -0
  81. package/dist/native/overlays/content.d.ts.map +1 -0
  82. package/dist/native/overlays/content.js +51 -0
  83. package/dist/native/overlays/content.js.map +1 -0
  84. package/dist/native/overlays/controller.d.ts +8 -0
  85. package/dist/native/overlays/controller.d.ts.map +1 -0
  86. package/dist/native/overlays/controller.js +285 -0
  87. package/dist/native/overlays/controller.js.map +1 -0
  88. package/dist/native/overlays/dialog-animation.d.ts +9 -0
  89. package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
  90. package/dist/native/overlays/dialog-animation.js +53 -0
  91. package/dist/native/overlays/dialog-animation.js.map +1 -0
  92. package/dist/native/overlays/dismiss-animation.d.ts +4 -0
  93. package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
  94. package/dist/native/overlays/dismiss-animation.js +18 -0
  95. package/dist/native/overlays/dismiss-animation.js.map +1 -0
  96. package/dist/native/overlays/focus.d.ts +5 -0
  97. package/dist/native/overlays/focus.d.ts.map +1 -0
  98. package/dist/native/overlays/focus.js +59 -0
  99. package/dist/native/overlays/focus.js.map +1 -0
  100. package/dist/native/overlays/popover.d.ts +12 -0
  101. package/dist/native/overlays/popover.d.ts.map +1 -0
  102. package/dist/native/overlays/popover.js +110 -0
  103. package/dist/native/overlays/popover.js.map +1 -0
  104. package/dist/native/overlays/styles.d.ts +11 -0
  105. package/dist/native/overlays/styles.d.ts.map +1 -0
  106. package/dist/native/overlays/styles.js +167 -0
  107. package/dist/native/overlays/styles.js.map +1 -0
  108. package/dist/native/prehide.d.ts +8 -0
  109. package/dist/native/prehide.d.ts.map +1 -0
  110. package/dist/native/prehide.js +238 -0
  111. package/dist/native/prehide.js.map +1 -0
  112. package/dist/native/runtime.d.ts +7 -0
  113. package/dist/native/runtime.d.ts.map +1 -0
  114. package/dist/native/runtime.js +776 -0
  115. package/dist/native/runtime.js.map +1 -0
  116. package/dist/native/shared/bridge.d.ts +6 -0
  117. package/dist/native/shared/bridge.d.ts.map +1 -0
  118. package/dist/native/shared/bridge.js +18 -0
  119. package/dist/native/shared/bridge.js.map +1 -0
  120. package/dist/native/shared/candidate.d.ts +25 -0
  121. package/dist/native/shared/candidate.d.ts.map +1 -0
  122. package/dist/native/shared/candidate.js +106 -0
  123. package/dist/native/shared/candidate.js.map +1 -0
  124. package/dist/native/shared/clipping.d.ts +5 -0
  125. package/dist/native/shared/clipping.d.ts.map +1 -0
  126. package/dist/native/shared/clipping.js +26 -0
  127. package/dist/native/shared/clipping.js.map +1 -0
  128. package/dist/native/shared/control-options.d.ts +6 -0
  129. package/dist/native/shared/control-options.d.ts.map +1 -0
  130. package/dist/native/shared/control-options.js +17 -0
  131. package/dist/native/shared/control-options.js.map +1 -0
  132. package/dist/native/shared/crossfade.d.ts +12 -0
  133. package/dist/native/shared/crossfade.d.ts.map +1 -0
  134. package/dist/native/shared/crossfade.js +67 -0
  135. package/dist/native/shared/crossfade.js.map +1 -0
  136. package/dist/native/shared/diagnostics.d.ts +4 -0
  137. package/dist/native/shared/diagnostics.d.ts.map +1 -0
  138. package/dist/native/shared/diagnostics.js +18 -0
  139. package/dist/native/shared/diagnostics.js.map +1 -0
  140. package/dist/native/shared/dom.d.ts +46 -0
  141. package/dist/native/shared/dom.d.ts.map +1 -0
  142. package/dist/native/shared/dom.js +180 -0
  143. package/dist/native/shared/dom.js.map +1 -0
  144. package/dist/native/shared/events.d.ts +15 -0
  145. package/dist/native/shared/events.d.ts.map +1 -0
  146. package/dist/native/shared/events.js +17 -0
  147. package/dist/native/shared/events.js.map +1 -0
  148. package/dist/native/shared/icons.d.ts +8 -0
  149. package/dist/native/shared/icons.d.ts.map +1 -0
  150. package/dist/native/shared/icons.js +61 -0
  151. package/dist/native/shared/icons.js.map +1 -0
  152. package/dist/native/shared/modal.d.ts +10 -0
  153. package/dist/native/shared/modal.d.ts.map +1 -0
  154. package/dist/native/shared/modal.js +160 -0
  155. package/dist/native/shared/modal.js.map +1 -0
  156. package/dist/native/shared/projection-id.d.ts +4 -0
  157. package/dist/native/shared/projection-id.d.ts.map +1 -0
  158. package/dist/native/shared/projection-id.js +7 -0
  159. package/dist/native/shared/projection-id.js.map +1 -0
  160. package/dist/native/shared/toolbar-options.d.ts +13 -0
  161. package/dist/native/shared/toolbar-options.d.ts.map +1 -0
  162. package/dist/native/shared/toolbar-options.js +28 -0
  163. package/dist/native/shared/toolbar-options.js.map +1 -0
  164. package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
  165. package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
  166. package/dist/native/shared/vertical-bars-geometry.js +85 -0
  167. package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
  168. package/dist/native/startup.d.ts +8 -0
  169. package/dist/native/startup.d.ts.map +1 -0
  170. package/dist/native/startup.js +70 -0
  171. package/dist/native/startup.js.map +1 -0
  172. package/dist/native/vertical-bars-web-search.d.ts +10 -0
  173. package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
  174. package/dist/native/vertical-bars-web-search.js +90 -0
  175. package/dist/native/vertical-bars-web-search.js.map +1 -0
  176. package/dist/native/vertical-bars-web.d.ts +5 -0
  177. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  178. package/dist/native/vertical-bars-web.js +422 -0
  179. package/dist/native/vertical-bars-web.js.map +1 -0
  180. package/dist/native-integration/index.d.ts +25 -0
  181. package/dist/native-integration/index.d.ts.map +1 -0
  182. package/dist/native-integration/index.js +33 -0
  183. package/dist/native-integration/index.js.map +1 -0
  184. package/dist/vertical-bars.d.ts +6 -0
  185. package/dist/vertical-bars.js +5 -0
  186. package/docs/appearance.md +45 -0
  187. package/docs/configuration.md +219 -0
  188. package/docs/framework-independent.md +107 -0
  189. package/docs/ionic-vertical-bars.md +173 -0
  190. package/docs/migration.md +184 -0
  191. package/docs/native-ui-extensions.md +394 -0
  192. package/docs/native-ui.md +528 -0
  193. package/docs/setup.md +224 -0
  194. package/docs/vertical-bars.md +431 -0
  195. package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
  196. package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
  197. package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
  198. package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
  199. package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
  200. package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
  201. package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
  202. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
  203. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
  204. package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
  205. package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
  206. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
  207. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
  208. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
  209. package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
  210. package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
  211. package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
  212. package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
  213. package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
  214. package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
  215. package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
  216. package/package.json +89 -4
  217. package/src/core.ts +14 -0
  218. package/src/native/activation.ts +385 -0
  219. package/src/native/components/index.ts +45 -0
  220. package/src/native/components/ion-back-button.ts +22 -0
  221. package/src/native/components/ion-button.ts +25 -0
  222. package/src/native/components/ion-buttons.ts +37 -0
  223. package/src/native/components/ion-fab.ts +151 -0
  224. package/src/native/components/ion-menu-button.ts +37 -0
  225. package/src/native/components/ion-segment.ts +27 -0
  226. package/src/native/components/ion-tab-bar.ts +50 -0
  227. package/src/native/components/searchable-tabs.ts +499 -0
  228. package/src/native/definitions.ts +274 -0
  229. package/src/native/extensions.ts +176 -0
  230. package/src/native/foldable/fold-state.ts +10 -0
  231. package/src/native/foldable/types.ts +4 -0
  232. package/src/native/html.ts +199 -0
  233. package/src/native/index.ts +32 -0
  234. package/src/native/legacy-layout.ts +21 -0
  235. package/src/native/overlays/content.ts +51 -0
  236. package/src/native/overlays/controller.ts +349 -0
  237. package/src/native/overlays/dialog-animation.ts +53 -0
  238. package/src/native/overlays/dismiss-animation.ts +15 -0
  239. package/src/native/overlays/focus.ts +64 -0
  240. package/src/native/overlays/popover.ts +120 -0
  241. package/src/native/overlays/styles.ts +181 -0
  242. package/src/native/prehide.ts +285 -0
  243. package/src/native/runtime.ts +979 -0
  244. package/src/native/shared/bridge.ts +18 -0
  245. package/src/native/shared/candidate.ts +162 -0
  246. package/src/native/shared/clipping.ts +24 -0
  247. package/src/native/shared/control-options.ts +20 -0
  248. package/src/native/shared/crossfade.ts +67 -0
  249. package/src/native/shared/diagnostics.ts +13 -0
  250. package/src/native/shared/dom.ts +304 -0
  251. package/src/native/shared/events.ts +14 -0
  252. package/src/native/shared/icons.ts +57 -0
  253. package/src/native/shared/modal.ts +157 -0
  254. package/src/native/shared/projection-id.ts +2 -0
  255. package/src/native/shared/toolbar-options.ts +49 -0
  256. package/src/native/shared/vertical-bars-geometry.ts +80 -0
  257. package/src/native/startup.ts +68 -0
  258. package/src/native/vertical-bars-web-search.ts +86 -0
  259. package/src/native/vertical-bars-web.ts +500 -0
  260. package/src/native-integration/index.ts +49 -0
  261. package/src/vertical-bars.ts +12 -0
package/docs/setup.md ADDED
@@ -0,0 +1,224 @@
1
+ # Set up Native UI in an Ionic app
2
+
3
+ Keep your existing Web appearance and render supported Ionic controls natively on iOS. This guide does not require iOS theme stylesheets.
4
+
5
+ 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.
6
+
7
+ Read [how projection works](../README.md#how-projection-works) first if you are new to Native UI. This guide connects your existing Ionic controls to that presentation.
8
+
9
+ ## Before you start
10
+
11
+ | Requirement | Ordinary native controls |
12
+ | ------------------------- | -------------------------------------------------------------------------- |
13
+ | Ionic | `>=8.8.1 <10` |
14
+ | Native project | Capacitor 8 with an iOS project |
15
+ | Build tools | Xcode 26 or later |
16
+ | Device or simulator | iOS 26 or later |
17
+ | Native dependency manager | Keep your existing SPM or CocoaPods integration. |
18
+ | Vertical Bars | See the separate [device requirements](vertical-bars.md#native-rendering). |
19
+
20
+ 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.
21
+
22
+ ## 1. Install the plugin
23
+
24
+ Keep your Capacitor packages on version 8. Add these direct application dependencies:
25
+
26
+ ```sh
27
+ npm install @rdlabo/capacitor-native-ui
28
+ ```
29
+
30
+ 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.
31
+
32
+ ## 2. Enable the shell
33
+
34
+ Call once from application startup after `ion-app` is mounted:
35
+
36
+ ```ts
37
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
38
+
39
+ const shell = await enableNativeUI();
40
+ ```
41
+
42
+ 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.
43
+
44
+ Existing Ionic markup supplies labels, icons, disabled state, and actions. For example, an existing fixed page header can contain:
45
+
46
+ ```html
47
+ <ion-header>
48
+ <ion-toolbar>
49
+ <ion-title>My app</ion-title>
50
+ <ion-buttons slot="end">
51
+ <ion-button aria-label="Add">
52
+ <ion-icon slot="icon-only" name="add"></ion-icon>
53
+ </ion-button>
54
+ </ion-buttons>
55
+ </ion-toolbar>
56
+ </ion-header>
57
+ ```
58
+
59
+ 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.
60
+
61
+ ### Place startup in your framework's app root
62
+
63
+ 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:
64
+
65
+ ```ts
66
+ // native-ui.ts — place beside your root component
67
+ import { enableNativeUI, type NativeUIHandle } from '@rdlabo/capacitor-native-ui';
68
+ // Without Ionic, use @rdlabo/capacitor-native-ui/core instead.
69
+
70
+ let lifecycle = Promise.resolve();
71
+ const report = (error: unknown) => console.error('Native UI lifecycle failed', error);
72
+
73
+ export function mountNativeUI(): () => void {
74
+ let disposed = false;
75
+ let shell: NativeUIHandle | undefined;
76
+ lifecycle = lifecycle
77
+ .then(async () => {
78
+ if (disposed) return;
79
+ shell = await enableNativeUI(); // Put your options here.
80
+ if (disposed) await shell.destroy();
81
+ })
82
+ .catch(report);
83
+ return () => {
84
+ disposed = true;
85
+ lifecycle = lifecycle
86
+ .then(async () => {
87
+ await shell?.destroy();
88
+ shell = undefined;
89
+ })
90
+ .catch(report);
91
+ };
92
+ }
93
+ ```
94
+
95
+ Choose **one** framework below. The helper belongs to your application; it is not another package to install.
96
+
97
+ **Angular:** call from the root component after the first DOM render, and register cleanup with its `DestroyRef`:
98
+
99
+ ```ts
100
+ import { afterNextRender, DestroyRef, inject } from '@angular/core';
101
+ import { mountNativeUI } from './native-ui';
102
+
103
+ // In the constructor of your root AppComponent:
104
+ const destroyRef = inject(DestroyRef);
105
+ afterNextRender(() => {
106
+ const stop = mountNativeUI();
107
+ destroyRef.onDestroy(stop);
108
+ });
109
+ ```
110
+
111
+ **React:** use one effect in the root component that renders `IonApp`:
112
+
113
+ ```tsx
114
+ import { useEffect } from 'react';
115
+ import { mountNativeUI } from './native-ui';
116
+
117
+ // Inside your root App component:
118
+ useEffect(() => mountNativeUI(), []);
119
+ ```
120
+
121
+ **Vue:** register lifecycle hooks in the root component's `<script setup>`:
122
+
123
+ ```ts
124
+ import { onMounted, onBeforeUnmount } from 'vue';
125
+ import { mountNativeUI } from './native-ui';
126
+
127
+ let stop: (() => void) | undefined;
128
+ onMounted(() => {
129
+ stop = mountNativeUI();
130
+ });
131
+ onBeforeUnmount(() => {
132
+ stop?.();
133
+ });
134
+ ```
135
+
136
+ These hooks manage the app's lifetime, not individual page transitions. Configure navigation below as well.
137
+
138
+ ## 3. Coordinate navigation
139
+
140
+ For optional Ionic animation coordination, install `@rdlabo/ionic-theme-ios27` and update Native UI together so their versions match. Configure navigation before Ionic initializes. Its adapter can retain Ionic's default transitions without loading theme CSS:
141
+
142
+ ```sh
143
+ npm install @rdlabo/ionic-theme-ios27 @rdlabo/capacitor-native-ui
144
+ ```
145
+
146
+ ```ts
147
+ import { iosTransitionAnimation, mdTransitionAnimation, type AnimationBuilder } from '@ionic/core';
148
+ import { withNativeUITransition } from '@rdlabo/ionic-theme-ios27';
149
+
150
+ const defaultTransition: AnimationBuilder = (baseEl, opts) =>
151
+ (opts.mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation)(baseEl, opts);
152
+
153
+ const ionicConfig = {
154
+ navAnimation: withNativeUITransition(defaultTransition),
155
+ };
156
+ ```
157
+
158
+ 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.
159
+
160
+ For a plugin-only app, use [navigation without the theme](#navigation-without-the-theme) below. Configure one approach before testing page navigation.
161
+
162
+ ## 4. Build, sync, and run
163
+
164
+ Run your application's Web build, then sync:
165
+
166
+ ```sh
167
+ npx cap sync ios
168
+ ```
169
+
170
+ 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.
171
+
172
+ | If you see… | Check… |
173
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
174
+ | 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. |
175
+ | Only one control remains on the Web | Compare its markup and styles with [supported controls](native-ui.md#supported-markup). |
176
+ | Controls overlap during navigation | Configure the transition adapter or suspend/resume lifecycle before navigating. |
177
+ | Missing styles or spacing | Retain the app's Web CSS; the plugin does not supply page layout. |
178
+
179
+ ## Next steps
180
+
181
+ - [Vertical Bars only](ionic-vertical-bars.md): a step-by-step rail setup with your existing theme.
182
+ - [Configuration](configuration.md): choose controls and change rail settings at runtime.
183
+ - [Custom SwiftUI or Compose content](native-ui-extensions.md): create an app-local extension.
184
+ - [Migration](migration.md): update older package names, imports, and native projects.
185
+
186
+ ## Navigation without the theme
187
+
188
+ 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:
189
+
190
+ ```ts
191
+ const suspension = await shell.suspend();
192
+ try {
193
+ await navigate();
194
+ } finally {
195
+ await suspension.resume();
196
+ }
197
+ ```
198
+
199
+ 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.
200
+
201
+ For a routerless HTML app, this is a complete animated page switch. A canceled animation restores the old page before resuming projection:
202
+
203
+ ```ts
204
+ async function showPage(current: HTMLElement, next: HTMLElement) {
205
+ const lease = await shell.suspend();
206
+ try {
207
+ current.hidden = true;
208
+ next.hidden = false;
209
+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
210
+ const animation = next.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150 });
211
+ try {
212
+ await animation.finished;
213
+ } catch {
214
+ next.hidden = true;
215
+ current.hidden = false;
216
+ }
217
+ }
218
+ } finally {
219
+ await lease.resume();
220
+ }
221
+ }
222
+ ```
223
+
224
+ 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.
@@ -0,0 +1,431 @@
1
+ ---
2
+ title: Vertical Bars
3
+ ---
4
+
5
+ # Vertical Bars
6
+
7
+ Vertical Bars moves eligible Ionic controls or explicitly marked HTML buttons into a side rail while keeping the original elements as the source of labels, icons and behavior. It works with the iOS theme or an existing Ionic theme, independently of hinge posture and the full Native UI.
8
+
9
+ Choose your setup before using this reference:
10
+
11
+ - **Ionic with a custom or default theme:** [Ionic rail setup](ionic-vertical-bars.md#try-it-in-your-existing-ionic-app).
12
+ - **No Ionic:** [HTML setup](framework-independent.md) and its [Web rail configuration](framework-independent.md#optional-web-rail). Use `/core` and your own layout; the Ionic setup below does not apply.
13
+ - **Existing iOS 27 theme and Native UI:** [Connect device placement](../../theme-ios27/docs/iphone-duo.md#project-controls-into-the-rail) using your existing runtime.
14
+
15
+ This page covers device-plugin integration, control eligibility, native appearance and runtime options. For posture-driven Ionic split panes, see [iPhone Duo support](../../theme-ios27/docs/iphone-duo.md).
16
+
17
+ Vertical Bars and iPhone Duo support are stable starting with version 2.0.0.
18
+
19
+ See [shared configuration](configuration.md) for all defaults and live `rail.enableVerticalControlArea()` / `rail.disableVerticalControlArea()` methods. [Plain HTML controls](framework-independent.md) use the same native rail renderers.
20
+
21
+ ## Enable Vertical Bars
22
+
23
+ Install the native package as a direct dependency:
24
+
25
+ ```sh
26
+ npm install @rdlabo/capacitor-native-ui
27
+ ```
28
+
29
+ For Capacitor iOS, build your Web assets and run `npx cap sync ios`. Supply the Web layout through your app or theme as described below.
30
+
31
+ For Ionic apps, start one runtime after `ion-app` is mounted:
32
+
33
+ ```ts
34
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
35
+
36
+ const rail = await enableNativeUI({ controls: 'verticalOnly' });
37
+ ```
38
+
39
+ Configure your Web rail layout and [apply device placement](#use-with-capacitor-foldable) for an iPhone Duo. The application owns placement and cleanup: call `await rail.destroy()` when its owner is disposed. If you already use `enableNativeUI()`, keep that runtime; it includes Vertical Bars. Do not start both.
40
+
41
+ The `/vertical-bars` entry point requires `@capacitor/core`, including in browser builds. Using only the theme’s rail CSS needs no native runtime. For native setup and navigation transitions, follow the [existing-theme guide](ionic-vertical-bars.md).
42
+
43
+ On supported Capacitor iOS, eligible controls use native SwiftUI rendering. Web, Android and unavailable native projection use Web clones. A browser preview can verify layout and actions, but cannot compare native button appearance.
44
+
45
+ ## Placement and Web layout
46
+
47
+ The application or theme owns Web spacing, rail positioning, and fallback styles. The plugin reads the supplied Ionic layout and placement; it does not reserve Web content space on its own. With the iOS27 theme, use its `setVerticalBarsLayout({ edge, inset })` helper. Native-only applications use their own CSS; no theme helper is required. Native availability comes from device metrics, independently of Web layout.
48
+
49
+ The [theme's optional rail stylesheet](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/vertical-bars-layout.md) supplies one ready-made layout. Its classes and CSS variables are not a requirement to install the theme package: applications may provide equivalent layout themselves. The runtime currently uses the `ios-theme-vertical-bars` class on `ion-app` to identify the rail layout, regardless of who supplies its styles.
50
+
51
+ ## Use with Capacitor Foldable
52
+
53
+ Use [`@erkamyaman/capacitor-foldable`](https://github.com/erkamyaman/capacitor-foldable) when your Web layout needs to follow the device's bar placement or hinge posture. Native UI does not subscribe to Foldable automatically.
54
+
55
+ | Owner | Responsibility |
56
+ | -------------------- | ------------------------------------------------------------------------------------------- |
57
+ | Foldable | Report initial device state and subsequent placement/posture changes. |
58
+ | Application or theme | Reserve Web space and choose the layout using the reported logical edge and measured inset. |
59
+ | Native UI | Check native rail availability independently and project eligible source controls. |
60
+
61
+ ```sh
62
+ npm install @erkamyaman/capacitor-foldable
63
+ npx cap sync ios
64
+ ```
65
+
66
+ ### With the theme's Ionic rail layout
67
+
68
+ After loading [the rail stylesheet](../../theme-ios27/docs/vertical-bars-layout.md) and mounting `ion-app`, start the runtime once and connect device placement:
69
+
70
+ ```ts
71
+ import { Foldable, type BarPlacement } from '@erkamyaman/capacitor-foldable';
72
+ import { setVerticalBarsLayout } from '@rdlabo/ionic-theme-ios27';
73
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
74
+
75
+ const rail = await enableNativeUI({ controls: 'verticalOnly' });
76
+ const applyPlacement = ({ verticalBarEdge, inset }: BarPlacement) => setVerticalBarsLayout({ edge: verticalBarEdge, inset });
77
+ const listener = await Foldable.addListener('barPlacementChange', applyPlacement);
78
+ applyPlacement(await Foldable.getBarPlacement());
79
+ ```
80
+
81
+ If a runtime is already active, keep it and add only the placement subscription. `setVerticalBarsLayout` resolves logical edges for RTL; a null edge restores the ordinary Web layout. Use the measured inset instead of a fixed width. Keep the theme's rail stylesheet; Foldable's `ionic-tabs.css` is not needed for this integration.
82
+
83
+ When the application owner is disposed, remove its subscription and destroy the runtime it owns:
84
+
85
+ ```ts
86
+ await listener.remove();
87
+ await rail.destroy();
88
+ ```
89
+
90
+ ### With application-owned CSS or without Ionic
91
+
92
+ Use the same `getBarPlacement()` / `barPlacementChange` subscription, but apply the edge and inset to your own CSS instead of calling `setVerticalBarsLayout`. A null edge means your layout should restore its ordinary spacing. For Ionic, retain the rail layout classes described above; for plain HTML, use the `/core` entry and [source placement and an optional Web rail host](framework-independent.md#optional-web-rail). Neither Foldable nor Native UI supplies the application's CSS.
93
+
94
+ Keep Web layout updates separate from native availability: do not pass Foldable's edge through the deprecated `nativeEdge` option. Native UI reads that capability itself. A browser layout preview does not require Foldable.
95
+
96
+ For hinge-driven content layout, read `getFoldState()` and subscribe to `foldStateChange` separately. Hinge posture does not select controls or enable the rail; see the theme's [posture subscription and split-pane example](../../theme-ios27/docs/iphone-duo.md#read-the-device-layout).
97
+
98
+ ## Native rendering
99
+
100
+ On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Without a device-reported edge, projection retains its Web fallback. A placement preference cannot enable an unsupported native rail. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
101
+
102
+ The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. Overlay layout follows the [optional theme rail layout](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/vertical-bars-layout.md#reserve-the-vertical-rail) when that stylesheet is used; custom layouts remain the application’s responsibility.
103
+
104
+ ### Native popovers (Vertical Bars only)
105
+
106
+ Native popover presentation is limited to popovers anchored to accepted native Vertical Bars `ion-button` controls. It is available with the standalone Vertical Bars runtime; the full Native UI is not required. Enable it explicitly when starting the runtime (`popover` defaults to `false`):
107
+
108
+ ```ts
109
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
110
+
111
+ const rail = await enableNativeUI({
112
+ controls: 'verticalOnly',
113
+ ionic: { verticalBars: { popover: true } },
114
+ });
115
+ ```
116
+
117
+ Use an `ion-popover` whose `trigger` identifies the projected button, or pass that button's click event to `PopoverController.create({ event, ... })`. Ordinary toolbar and body popovers remain on the Web. If a rail-anchored popover falls back to Web rendering, the rail also returns to Web projection until dismissal. See [native popover behavior and limitations](native-ui.md#popovers-anchored-to-vertical-bars).
118
+
119
+ ## Toolbar actions
120
+
121
+ A standard `ion-back-button` can project from outside a fixed toolbar, including routed content or a persistent app shell. `ion-menu-button` and other toolbar actions require a fixed toolbar.
122
+
123
+ By default, an `ion-button` moves into the rail when it contains an `ion-icon` or SVG with `slot="icon-only"`. The button must be in a fixed `ion-toolbar` directly inside `ion-header` or `ion-footer`, outside scrolling `ion-content`.
124
+
125
+ | Icon markup | Placement |
126
+ | ---------------------------------------- | --------------------------- |
127
+ | `slot="icon-only"` | Vertical rail |
128
+ | `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
129
+ | No icon | Original horizontal toolbar |
130
+
131
+ ```html
132
+ <ion-header>
133
+ <ion-toolbar>
134
+ <ion-buttons slot="end">
135
+ <ion-button aria-label="Done">
136
+ <ion-icon name="checkmark-outline" slot="icon-only"></ion-icon>
137
+ </ion-button>
138
+ </ion-buttons>
139
+ </ion-toolbar>
140
+ </ion-header>
141
+ ```
142
+
143
+ All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors follow this placement rule. Keep accessible names and the original click or form-submit handlers on the source buttons. `type="submit"` and `.button-submit` do not select a different placement.
144
+
145
+ The rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
146
+
147
+ A toolbar whose content has all moved into the rail collapses while projection is active. Toolbars containing a title, direct text, other content or a horizontal-only control remain visible. Removing projection restores the original toolbar.
148
+
149
+ ### Select actions and grouping independently
150
+
151
+ Use `controls` to configure toolbar selection and grouping. Each field accepts `true`, `false`, `'horizontalOnly'`, or `'verticalOnly'`, with omitted fields defaulting to `true`. Use `controls: 'verticalOnly'` for all default selection in the rail. Local attributes can override selection; explicit sources and extensions remain independent. See [all selection defaults](configuration.md#selection-defaults).
152
+
153
+ | Option | Responsibility | What it does not do |
154
+ | ------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
155
+ | `button` | Automatically select eligible `ion-button` and `ion-menu-button` actions, whether inside or outside `ion-buttons`. | Does not select back buttons. |
156
+ | `buttonGroup` | Allow selected actions in `ion-buttons` to project as a group when the existing grouping rules allow it. | Does not select, enable or disable any child action. |
157
+ | `backButton` | Automatically select eligible `ion-back-button` elements, independently of their parent group. | Does not group back buttons with other actions. |
158
+
159
+ For ordinary icon-only buttons in a group, with no local attributes:
160
+
161
+ | `buttonGroup` | `button` | Result |
162
+ | ------------- | -------- | ------------------------------------------------------------ |
163
+ | `true` | `true` | Eligible buttons project together when the group qualifies. |
164
+ | `false` | `true` | Eligible buttons project individually. |
165
+ | `true` | `false` | No ordinary buttons project; grouping alone selects nothing. |
166
+ | `false` | `false` | No ordinary buttons project. |
167
+
168
+ These results concern ordinary actions only. Back navigation, tabs and native
169
+ search are independent. For example, to select only back navigation among toolbar
170
+ actions:
171
+
172
+ ```ts
173
+ const rail = await enableNativeUI({
174
+ controls: { buttonGroup: false, button: false, backButton: 'verticalOnly', tabs: 'verticalOnly', segment: false, fab: false },
175
+ });
176
+ ```
177
+
178
+ This does not disable tab projection or tab rail CSS.
179
+
180
+ ### Explicit action selection and local grouping
181
+
182
+ Use `data-vertical-bars="true|false"` on the element whose behavior you want to
183
+ override. The attribute is **not inherited**.
184
+
185
+ | Element | `data-vertical-bars="true"` | `data-vertical-bars="false"` | Default when absent or invalid |
186
+ | -------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- | -------------------------------------------------- |
187
+ | `ion-button` / `ion-menu-button` | Select the action, including supported text actions without an icon-only slot. | Exclude the action from the rail. | `controls.button` and automatic shape rules. |
188
+ | `ion-buttons` | Allow grouping of already-selected actions. | Project selected actions individually. | `controls.buttonGroup` and grouping eligibility. |
189
+ | `ion-back-button` | Select back navigation independently of its group. | Exclude this back button from the rail. | `controls.backButton` and back-button eligibility. |
190
+ | `ion-tab-bar` | Select eligible rail tabs and their native search presentation. | Disable native rail tab selection; existing Web layout remains. | `controls.tabs` and tab-bar eligibility. |
191
+
192
+ The group's attribute never selects or excludes its children. All opt-ins remain subject to structural exclusions and rail availability.
193
+
194
+ Empty or unknown attribute values are ignored. To disable automatic action
195
+ selection and explicitly select a text button:
196
+
197
+ ```ts
198
+ const rail = await enableNativeUI({
199
+ controls: { buttonGroup: 'verticalOnly', button: false, backButton: false, tabs: 'verticalOnly', segment: false, fab: false },
200
+ });
201
+ ```
202
+
203
+ ```html
204
+ <ion-buttons>
205
+ <ion-button data-vertical-bars="true">Save</ion-button>
206
+ <ion-button>Cancel</ion-button>
207
+ </ion-buttons>
208
+ ```
209
+
210
+ Only Save moves; Cancel stays in the horizontal toolbar. Adding
211
+ `data-vertical-bars="true"` to the group would not select Cancel.
212
+
213
+ To group two explicitly selected actions even when `buttonGroup: false`:
214
+
215
+ ```html
216
+ <ion-buttons data-vertical-bars="true">
217
+ <ion-button data-vertical-bars="true">Save</ion-button>
218
+ <ion-button data-vertical-bars="true">Done</ion-button>
219
+ </ion-buttons>
220
+ ```
221
+
222
+ Set the group's attribute to `false` to project the same actions individually.
223
+ Grouping keeps the existing eligibility rules: the entire group must qualify,
224
+ and a single ordinary button projects on its own. A group with horizontal
225
+ content projects its selected actions individually. The source group and its
226
+ unselected children remain in their original toolbar.
227
+
228
+ Explicit action selection still requires a fixed header/footer toolbar.
229
+ Shell opt-outs, horizontal-only classes and overlay restrictions still apply.
230
+ Placement and grouping are captured for the routed-page visit: attribute changes
231
+ take effect after leaving and re-entering the page. Restart the runtime to change
232
+ startup options.
233
+
234
+ ### Back buttons inside groups
235
+
236
+ An `ion-back-button` remains a navigation action even inside `ion-buttons`.
237
+ It ignores `buttonGroup`, `button`, and the group's `data-vertical-bars` attribute.
238
+ Its own attribute overrides `backButton`. It uses the dedicated back position,
239
+ never an ordinary action group, and is not duplicated as a toolbar action.
240
+
241
+ ```html
242
+ <ion-buttons data-vertical-bars="false">
243
+ <!-- Still projects when backButton is true. -->
244
+ <ion-back-button></ion-back-button>
245
+ </ion-buttons>
246
+ ```
247
+
248
+ The Ionic DOM container does not redefine the navigation role. The theme forwards
249
+ the projected back action to Ionic; it does not replace Ionic routing with a
250
+ native navigation stack. Native layout remains system-managed.
251
+
252
+ Existing back-button eligibility still applies, including the supported default
253
+ icon/color and page-position rules. Ancestor `data-native-ui="disabled"`,
254
+ `.ios-theme-horizontal-only` on `ion-buttons`, and other structural exclusions
255
+ remain opt-outs; those are distinct from the grouping setting.
256
+
257
+ ### Placement versus appearance
258
+
259
+ | Setting | Values | Responsibility | Applies to |
260
+ | -------------------------- | ----------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
261
+ | `data-native-ui` | `button`, `back-button`, `disabled` | Declare an explicit source or exclude a subtree. | [Explicit source elements](framework-independent.md#2-mark-source-controls); `disabled` also applies to ancestors. |
262
+ | `data-native-ui-placement` | `element`, `rail` | Choose the original element position or request the rail. | Explicit sources; `element` also keeps automatic Ionic controls at their original position. |
263
+ | `data-vertical-bars` | `true`, `false` | Override rail selection or grouping defaults. | Ionic actions and groups listed above. |
264
+ | `data-horizontal-bars` | `true`, `false` | Override ordinary-placement selection or grouping defaults. | [Ordinary Ionic controls](native-ui.md#toolbar-selection-and-grouping). |
265
+ | `data-projection` | `system`, `source` | Choose native appearance without selecting an action. | Native vertical `ion-button` and `ion-menu-button`; inherited from the nearest `ion-buttons`. |
266
+
267
+ Selection attributes are not inherited. Appearance follows the button-then-group priority described below. An attribute never makes unsupported markup or a missing native rail available.
268
+
269
+ The rail shares one light/dark appearance across projected controls. See [Shared Vertical Bars appearance](appearance.md#shared-vertical-bars-appearance) when configuring your Web theme.
270
+
271
+ ### Choose button appearance
272
+
273
+ Use `ionic.verticalBars.buttonProjection` to choose native button appearance.
274
+
275
+ For native vertical `ion-button` and `ion-menu-button` actions, choose who controls appearance:
276
+
277
+ | `buttonProjection` | Appearance |
278
+ | -------------------- | ----------------------------------------------------------------------------------------------------------- |
279
+ | `'system'` (default) | SwiftUI styles the buttons and tints their icons. Ionic fill, colors and borders are not applied. |
280
+ | `'source'` | Projects the supported Ionic fill and computed colors described in [Source fill rules](#source-fill-rules). |
281
+
282
+ ```ts
283
+ const rail = await enableNativeUI({ controls: 'verticalOnly', ionic: { verticalBars: { buttonProjection: 'source' } } });
284
+ ```
285
+
286
+ `enableNativeUI()` and the standalone `enableVerticalControlArea()` accept these settings inside `ionic.verticalBars`. Use one runtime, and destroy it before restarting with different options. Either mode preserves actions, disabled state and grouping. Disabled icons use the native disabled appearance. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
287
+
288
+ ### Override individual buttons or groups
289
+
290
+ Use `data-projection` for local exceptions. Existing classes remain supported:
291
+
292
+ | Attribute | Equivalent class |
293
+ | -------------------------- | ----------------------------- |
294
+ | `data-projection="source"` | `ios-theme-projection-source` |
295
+ | `data-projection="system"` | `ios-theme-projection-system` |
296
+
297
+ ```html
298
+ <ion-buttons data-projection="source">
299
+ <ion-button fill="solid" aria-label="Add">
300
+ <ion-icon name="add-outline" slot="icon-only"></ion-icon>
301
+ </ion-button>
302
+ <ion-button data-projection="system" aria-label="Search">
303
+ <ion-icon name="search-outline" slot="icon-only"></ion-icon>
304
+ </ion-button>
305
+ </ion-buttons>
306
+ ```
307
+
308
+ The first matching setting wins:
309
+
310
+ 1. The button's local setting.
311
+ 2. Its nearest `ion-buttons` local setting.
312
+ 3. The startup `buttonProjection` option, or `system` if omitted.
313
+
314
+ On the same element, a valid `data-projection` value takes precedence over the classes. Empty or unknown values are ignored. Without a valid attribute, `system` wins if both classes are present. Removing an attribute falls back to the element's classes, then the next level above. Attribute and class changes apply without restarting the runtime; grouping and placement stay unchanged.
315
+
316
+ Only native vertical `ion-button` and `ion-menu-button` actions interpret these settings. Other ancestors, back buttons, tabs and FABs do not. They do not change Web styling. To keep a control or subtree on the Web entirely, use [`data-native-ui="disabled"`](native-ui.md#supported-markup).
317
+
318
+ ### Source fill rules
319
+
320
+ For an `ion-button` resolved to `source`, fill is chosen separately from the projection mode:
321
+
322
+ | Button | Effective fill |
323
+ | ------------------------------------------------------ | ------------------- |
324
+ | Explicit `fill="clear"`, `"solid"` or `"outline"` | The explicit value |
325
+ | Omitted fill or `fill="default"` inside `ion-buttons` | `clear` |
326
+ | Omitted fill or `fill="default"` outside `ion-buttons` | `buttonDefaultFill` |
327
+
328
+ `verticalBars.buttonDefaultFill` accepts `'solid'` or `'glass'`; omission is equivalent to `'glass'`. Use `'solid'` for Ionic's default button design, or `'glass'` for a glass design. It applies to every button resolved to `source`, including a local exception under a global `system` setting.
329
+
330
+ ```ts
331
+ // Keep system styling globally; local source buttons use Ionic's default fill.
332
+ const rail = await enableNativeUI({ controls: 'verticalOnly', ionic: { verticalBars: { buttonDefaultFill: 'solid' } } });
333
+ ```
334
+
335
+ ```html
336
+ <!-- In a fixed toolbar, outside ion-buttons: omitted fill resolves to solid. -->
337
+ <ion-button data-projection="source" aria-label="Add">
338
+ <ion-icon name="add-outline" slot="icon-only"></ion-icon>
339
+ </ion-button>
340
+ ```
341
+
342
+ | Effective fill | Native appearance in `source` mode |
343
+ | -------------- | ------------------------------------------------------------------- |
344
+ | `clear` | Icon color, without a glass background; CSS backgrounds are ignored |
345
+ | `solid` | Computed background color tints a prominent glass button |
346
+ | `outline` | Computed border color and width, with native glass |
347
+ | `null` | Native glass with source icon colors |
348
+
349
+ Inside `ion-buttons`, explicitly set `fill="solid"` to project a background; `buttonDefaultFill: 'solid'` does not override the group's clear default. Native Liquid Glass tinting can differ from the CSS color, especially for translucent backgrounds. With the iOS theme, ordinary `ion-buttons` retain group projection; `ion-buttons.ios-theme-disabled` projects eligible buttons individually. Local projection settings do not change that grouping rule.
350
+
351
+ ## Tab bar
352
+
353
+ When the app contains `ion-tabs`, its tab bar moves into the reserved region and uses the native Duo edge spacing; the Ionic `slot` value does not select a different position. Without native projection, the stable Web rail is icon-only, matching the native resting presentation. While the user presses and drags across that rail, every icon-and-label tab reveals its label so the pending destination stays identifiable. The Web tab bar receives pointer input in the simulated system region. Native tabs and a restored Web tab bar fade in over 180ms; disappearance remains immediate. Reduced motion disables this fade. Use `ion-menu` when navigation should become a sidebar; this mode does not convert tabs into a menu. Web clones also work when no `ion-tabs` exists. Disabling the mode or leaving the page removes the native ownership or Web clones and restores their sources. Override `--ios-theme-vertical-bars-toolbar-top` when the simulated system controls use a different vertical layout.
354
+
355
+ ## Vertical Control Area API
356
+
357
+ The generated reference below documents the handle returned by `enableVerticalControlArea()`.
358
+
359
+ <docgen-index>
360
+
361
+ * [`setPlacement(...)`](#setplacement)
362
+ * [`enableVerticalControlArea()`](#enableverticalcontrolarea)
363
+ * [`disableVerticalControlArea()`](#disableverticalcontrolarea)
364
+ * [Interfaces](#interfaces)
365
+ * [Type Aliases](#type-aliases)
366
+
367
+ </docgen-index>
368
+
369
+ <docgen-api>
370
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
371
+
372
+ ### setPlacement(...)
373
+
374
+ ```typescript
375
+ setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
376
+ ```
377
+
378
+ Legacy Web CSS adapter. Use the theme setVerticalBarsLayout helper instead.
379
+
380
+ | Param | Type |
381
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
382
+ | **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
383
+ | **`rtl`** | <code>boolean</code> |
384
+
385
+ --------------------
386
+
387
+
388
+ ### enableVerticalControlArea()
389
+
390
+ ```typescript
391
+ enableVerticalControlArea() => Promise<void>
392
+ ```
393
+
394
+ Enable rail projection when available, preserving other settings. Rejects after destroy.
395
+
396
+ --------------------
397
+
398
+
399
+ ### disableVerticalControlArea()
400
+
401
+ ```typescript
402
+ disableVerticalControlArea() => Promise<void>
403
+ ```
404
+
405
+ Restore rail sources to the Web without stopping ordinary native controls. Rejects after destroy.
406
+
407
+ --------------------
408
+
409
+
410
+ ### Interfaces
411
+
412
+
413
+ #### VerticalBarPlacement
414
+
415
+ | Prop | Type | Description |
416
+ | ---------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------- |
417
+ | **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
418
+ | **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
419
+ | **`nativeEdge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | Ignored. Native availability is read from the plugin’s own device metrics. |
420
+
421
+
422
+ ### Type Aliases
423
+
424
+
425
+ #### VerticalBarEdge
426
+
427
+ Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
428
+
429
+ <code>'leading' | 'trailing' | null</code>
430
+
431
+ </docgen-api>