@rdlabo/capacitor-native-ui 1.2.1 → 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 (67) hide show
  1. package/README.md +38 -25
  2. package/dist/core.d.ts +2 -2
  3. package/dist/native/activation.d.ts.map +1 -1
  4. package/dist/native/activation.js +19 -19
  5. package/dist/native/activation.js.map +1 -1
  6. package/dist/native/definitions.d.ts +22 -40
  7. package/dist/native/definitions.d.ts.map +1 -1
  8. package/dist/native/html.d.ts.map +1 -1
  9. package/dist/native/html.js +10 -1
  10. package/dist/native/html.js.map +1 -1
  11. package/dist/native/index.d.ts +2 -2
  12. package/dist/native/index.d.ts.map +1 -1
  13. package/dist/native/index.js +2 -11
  14. package/dist/native/index.js.map +1 -1
  15. package/dist/native/overlays/controller.js +1 -1
  16. package/dist/native/overlays/controller.js.map +1 -1
  17. package/dist/native/prehide.js +1 -1
  18. package/dist/native/prehide.js.map +1 -1
  19. package/dist/native/runtime.d.ts +1 -1
  20. package/dist/native/runtime.d.ts.map +1 -1
  21. package/dist/native/runtime.js +8 -20
  22. package/dist/native/runtime.js.map +1 -1
  23. package/dist/native/shared/candidate.d.ts +2 -2
  24. package/dist/native/shared/candidate.d.ts.map +1 -1
  25. package/dist/native/shared/candidate.js +2 -2
  26. package/dist/native/shared/candidate.js.map +1 -1
  27. package/dist/native/shared/control-options.d.ts +6 -0
  28. package/dist/native/shared/control-options.d.ts.map +1 -0
  29. package/dist/native/shared/control-options.js +17 -0
  30. package/dist/native/shared/control-options.js.map +1 -0
  31. package/dist/native/shared/toolbar-options.d.ts +6 -2
  32. package/dist/native/shared/toolbar-options.d.ts.map +1 -1
  33. package/dist/native/shared/toolbar-options.js +7 -15
  34. package/dist/native/shared/toolbar-options.js.map +1 -1
  35. package/dist/native/startup.d.ts.map +1 -1
  36. package/dist/native/startup.js +0 -16
  37. package/dist/native/startup.js.map +1 -1
  38. package/dist/native/vertical-bars-web.d.ts.map +1 -1
  39. package/dist/native/vertical-bars-web.js +3 -2
  40. package/dist/native/vertical-bars-web.js.map +1 -1
  41. package/dist/vertical-bars.d.ts +2 -2
  42. package/docs/appearance.md +45 -0
  43. package/docs/configuration.md +106 -74
  44. package/docs/framework-independent.md +35 -10
  45. package/docs/ionic-vertical-bars.md +173 -0
  46. package/docs/migration.md +65 -87
  47. package/docs/native-ui-extensions.md +60 -31
  48. package/docs/native-ui.md +186 -67
  49. package/docs/setup.md +43 -24
  50. package/docs/vertical-bars.md +124 -72
  51. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +1 -1
  52. package/package.json +1 -1
  53. package/src/core.ts +1 -2
  54. package/src/native/activation.ts +13 -22
  55. package/src/native/definitions.ts +40 -90
  56. package/src/native/html.ts +11 -5
  57. package/src/native/index.ts +2 -11
  58. package/src/native/overlays/controller.ts +1 -1
  59. package/src/native/prehide.ts +1 -1
  60. package/src/native/runtime.ts +7 -16
  61. package/src/native/shared/candidate.ts +4 -4
  62. package/src/native/shared/control-options.ts +20 -0
  63. package/src/native/shared/toolbar-options.ts +8 -12
  64. package/src/native/startup.ts +0 -16
  65. package/src/native/vertical-bars-web.ts +4 -2
  66. package/src/vertical-bars.ts +2 -1
  67. package/docs/native-mini-player.md +0 -47
@@ -6,10 +6,10 @@ title: App-local Native UI extensions (experimental)
6
6
 
7
7
  > **Experimental:** The extension API, native registration interfaces and behavior may change in future releases. This applies to both SwiftUI and Kotlin/Compose extensions, including `element` and `tab-accessory` placement.
8
8
 
9
- > 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.
10
-
11
9
  An extension projects your existing Web UI into a SwiftUI View on iOS or Jetpack Compose content on Android. Keep both definitions in your application: no npm package, separate library, or Capacitor plugin is required. The Web element remains the source of truth and the fallback UI.
12
10
 
11
+ Extensions use the same [projection model](../README.md#how-projection-works) as built-in controls. Define the DOM data and actions to expose, then provide their native presentation.
12
+
13
13
  ## Before you start
14
14
 
15
15
  Start with an existing Capacitor 8 app and install the plugin if you have not already:
@@ -63,9 +63,18 @@ source.querySelector('button')!.addEventListener('click', () => {
63
63
  source.querySelector('[data-count-label]')!.textContent = `Count: ${count}`;
64
64
  });
65
65
  ```
66
- The shell observes DOM changes and sends the new props to the native content. State rendered into DOM attributes or text is observed automatically. For properties or external state read by the reader that do not mutate the DOM, call `shell.markForCheck()` after changing them.
67
66
 
68
- The reader is synchronous and read-only. Return `null` when the source should stay on the Web. Props are a JSON object (finite numbers, strings, booleans, null, arrays and objects), limited to 64K characters. Action names map to the source itself or its descendants; missing, hidden and disabled targets cannot be activated. Up to 30 actions are supported per source.
67
+ The shell observes DOM changes and sends the new props to the native content. State rendered into DOM attributes or text is observed automatically. For properties or external state read by the reader that do not mutate the DOM, call `shell.markForCheck()` after changing them.
68
+
69
+ ### Reader contract
70
+
71
+ | Part | Requirement |
72
+ | -------------- | -------------------------------------------------------------------------------------------------------------- |
73
+ | Execution | Synchronous and read-only. |
74
+ | Web fallback | Return `null` when the source should stay on the Web. |
75
+ | `props` | JSON object containing finite numbers, strings, booleans, null, arrays and objects; limited to 64K characters. |
76
+ | Action targets | The source itself or its descendants. Missing, hidden and disabled targets cannot be activated. |
77
+ | Action count | Up to 30 per source. |
69
78
 
70
79
  ## 2a. Register a SwiftUI View on iOS
71
80
 
@@ -221,14 +230,23 @@ For source-specific fallback reasons, temporarily enable [`debug: true`](configu
221
230
 
222
231
  ## Content and placement are independent
223
232
 
224
- | `placement` | Behavior |
225
- | --- | --- |
233
+ | `placement` | Behavior |
234
+ | ---------------------- | ------------------------------------------------------------------------------------- |
226
235
  | Omitted or `'element'` | Hosts native content in the source element's Web frame on iOS 26+ or Android API 24+. |
227
- | `'tab-accessory'` | Attaches the View to a projected tab bar's system SwiftUI tab accessory on iOS 26.1+. |
236
+ | `'tab-accessory'` | Attaches the View to a projected tab bar's system SwiftUI tab accessory on iOS 26.1+. |
237
+
238
+ Extensions are independent of `controls` and `ionic.auto`, including with `enableVerticalControlArea()`. The application controls their presentation: return `null` from the reader when a source should stay on the Web, then call `markForCheck()` when that condition changes. Accessories still require their supported native tab host.
228
239
 
229
- For `tab-accessory`, place the source inside its owning `ion-tabs`. The shell resolves that tabs element's `ion-tab-bar`; applications do not pass native controller references. Currently only the **Vertical Bars SwiftUI tab host** supports this placement. Ordinary UIKit tab bars, missing native tabs, iOS 26.0, or multiple competing accessories keep the source on the Web. The shell does not approximate a system accessory with a floating overlay. Keep an equivalent Web layout (for example, a fixed footer); the source retains its layout space while hidden.
240
+ ### Tab accessory requirements
230
241
 
231
- `enableVerticalControlArea({ extensions: [...] })` also accepts extensions. In this standalone mode only accessories belonging to native Vertical Bars are eligible; ordinary element projections require `enableNativeUI`.
242
+ | Requirement | Supported configuration |
243
+ | ------------------- | ------------------------------------------------------------------------------------------------- |
244
+ | Source location | Inside its owning `ion-tabs`; the shell resolves that element's `ion-tab-bar`. |
245
+ | Native host | Vertical Bars SwiftUI tab host on iOS 26.1+. |
246
+ | Accessory count | A single accessory for the host. |
247
+ | Web fallback layout | Keep an equivalent Web layout, such as a fixed footer; the source retains its space while hidden. |
248
+
249
+ Ordinary UIKit tab bars, missing native tabs, iOS 26.0 or competing accessories keep the source on the Web. Applications do not pass native controller references, and the shell does not approximate a system accessory with a floating overlay.
232
250
 
233
251
  ## Read source geometry and presentation state
234
252
 
@@ -246,16 +264,16 @@ try NativeUI.register("app.counter") {
246
264
 
247
265
  The existing `(props, actions)` closure remains supported. Both forms use the same registration names and duplicate-name checks.
248
266
 
249
- | Context property | Meaning |
250
- | --- | --- |
251
- | `sourceID` | Opaque source identity for its current lifetime; not the HTML `id`. |
252
- | `sourceFrame` | DOM `getBoundingClientRect()` in CSS pixels relative to the Web viewport, including `x`, `y`, `width`, and `height`. |
253
- | `sourceFrameInWebView` | The source rectangle converted to UIKit points in the WebView's local coordinate system; not screen coordinates. |
254
- | `viewportWidth` | CSS viewport width used for that snapshot's coordinate conversion. |
255
- | `pointsPerCSSPixel` | UIKit points per CSS pixel; not device pixel density. |
256
- | `placement` | `.element` or `.tabAccessory`. |
257
- | `colorScheme` | `.light` or `.dark`, also applied to the SwiftUI environment. |
258
- | `layoutDirection` | `.leftToRight` or `.rightToLeft`, also applied to the SwiftUI environment. |
267
+ | Context property | Meaning |
268
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
269
+ | `sourceID` | Opaque source identity for its current lifetime; not the HTML `id`. |
270
+ | `sourceFrame` | DOM `getBoundingClientRect()` in CSS pixels relative to the Web viewport, including `x`, `y`, `width`, and `height`. |
271
+ | `sourceFrameInWebView` | The source rectangle converted to UIKit points in the WebView's local coordinate system; not screen coordinates. |
272
+ | `viewportWidth` | CSS viewport width used for that snapshot's coordinate conversion. |
273
+ | `pointsPerCSSPixel` | UIKit points per CSS pixel; not device pixel density. |
274
+ | `placement` | `.element` or `.tabAccessory`. |
275
+ | `colorScheme` | `.light` or `.dark`, also applied to the SwiftUI environment. |
276
+ | `layoutDirection` | `.leftToRight` or `.rightToLeft`, also applied to the SwiftUI environment. |
259
277
 
260
278
  The closure receives a fresh context when the shell applies an updated snapshot, including source movement, resizing, and appearance changes. Use the current context rather than caching its geometry in `@State`. Updates cross the Web/native bridge; they are not a synchronous DOM measurement from Swift.
261
279
 
@@ -272,16 +290,16 @@ Do not apply `sourceFrame.x` or `y` as another SwiftUI offset: the shell already
272
290
 
273
291
  ## Use the shared projection capabilities
274
292
 
275
- | Capability | How an extension uses it |
276
- | --- | --- |
277
- | Position and size | Automatic host placement plus `NativeUIContext`; actual local bounds through `GeometryReader`. |
278
- | Dark mode and RTL | `context` or SwiftUI `@Environment(\.colorScheme)` and `@Environment(\.layoutDirection)`. |
279
- | Reduce Motion and accessibility | SwiftUI environment values such as `@Environment(\.accessibilityReduceMotion)` and native accessibility modifiers. Give custom controls accessible names. |
280
- | DOM-backed application state | Return JSON `props` from `read(element)`; DOM changes trigger synchronization. For changes without a DOM mutation, call `shell.markForCheck()`. |
281
- | Enabled/visible action targets | `actions.availableNames` or `actions.isAvailable("increment")`; unavailable DOM targets are omitted. |
282
- | Invoke the original Web action | `try await actions.activate("increment")`; completion and failure follow the contract below. |
283
- | Temporary Web fallback and teardown | Managed automatically for unsupported content, overlays, navigation suspension, and source removal. |
284
- | Application-controlled suspension | Use the shell handle's `suspend()`, the returned token's `resume()`, and `destroy()` from JavaScript. |
293
+ | Capability | How an extension uses it |
294
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
295
+ | Position and size | Automatic host placement plus `NativeUIContext`; actual local bounds through `GeometryReader`. |
296
+ | Dark mode and RTL | `context` or SwiftUI `@Environment(\.colorScheme)` and `@Environment(\.layoutDirection)`. |
297
+ | Reduce Motion and accessibility | SwiftUI environment values such as `@Environment(\.accessibilityReduceMotion)` and native accessibility modifiers. Give custom controls accessible names. |
298
+ | DOM-backed application state | Return JSON `props` from `read(element)`; DOM changes trigger synchronization. For changes without a DOM mutation, call `shell.markForCheck()`. |
299
+ | Enabled/visible action targets | `actions.availableNames` or `actions.isAvailable("increment")`; unavailable DOM targets are omitted. |
300
+ | Invoke the original Web action | `try await actions.activate("increment")`; completion and failure follow the contract below. |
301
+ | Temporary Web fallback and teardown | Managed automatically for unsupported content, overlays, navigation suspension, and source removal. |
302
+ | Application-controlled suspension | Use the shell handle's `suspend()`, the returned token's `resume()`, and `destroy()` from JavaScript. |
285
303
 
286
304
  For example, disable a native button when its original action is unavailable:
287
305
 
@@ -358,8 +376,19 @@ The shell owns discovery, geometry, state synchronization, native controller con
358
376
 
359
377
  Element placement requires the source to fit fully inside the viewport and its ancestors' overflow clipping bounds, including Ionic's shadow scroll containers. Partially clipped sources stay on the Web and become eligible again when scrolled fully into view. Tab accessories use the system host's placement instead.
360
378
 
361
- `try await actions.activate(name)` completes after the original Web element receives its click. It does **not** wait for application work launched by the click handler (network requests, navigation or modal presentation). Disabled, removed or stale actions reject; teardown, native task cancellation and bridge timeouts settle pending calls. The bridge acknowledgement is a Capacitor Promise method.
379
+ ### Action completion
380
+
381
+ | Event | Result of `try await actions.activate(name)` |
382
+ | ------------------------------------------------------------------ | -------------------------------------------- |
383
+ | Original Web element receives its click | Completes. |
384
+ | Handler starts a network request, navigation or modal presentation | Does not wait for that application work. |
385
+ | Action is disabled, removed or stale | Rejects. |
386
+ | Teardown, native task cancellation or bridge timeout | Settles pending calls. |
387
+
388
+ The bridge acknowledgement is a Capacitor Promise method.
362
389
 
363
390
  The [demo reader](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/native-ui/counter.ts) and [demo SwiftUI View](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/ios/App/App/NativeUICounter.swift) show both placements using app-local files. Open the Native UI demo page to compare native projection with its Web fallback.
364
391
 
365
- For a complete application feature, see the [SwiftUI mini-player example](native-mini-player.md): artwork, track metadata, playback controls and swipe-to-open, all defined in the demo application.
392
+ ## Example: mini-player
393
+
394
+ The demo mini-player shows the same pattern with track metadata and playback actions: a [DOM reader and Web fallback](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/native-ui/mini-player.ts) paired with an [app-local SwiftUI View](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/ios/App/App/NativeMiniPlayer.swift). The Web app owns playback state and modal presentation; native `open` and `toggle` actions activate its original buttons. This is a UI example, not an audio engine. Adapt the files after completing the extension setup above.
package/docs/native-ui.md CHANGED
@@ -2,15 +2,13 @@
2
2
  title: Native UI (Preview)
3
3
  ---
4
4
 
5
- For application-specific SwiftUI or Compose content, see [experimental App-local Native UI extensions](native-ui-extensions.md).
6
-
7
5
  # Native UI (Preview)
8
6
 
9
7
  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
8
 
11
9
  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
10
 
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).
11
+ Read [how projection works](../README.md#how-projection-works) for the shared DOM and native presentation model. 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
12
 
15
13
  ## Enable Native UI
16
14
 
@@ -39,54 +37,52 @@ import { configureNativeTransition } from '@rdlabo/capacitor-native-ui';
39
37
  await configureNativeTransition();
40
38
  ```
41
39
 
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.
40
+ ### Navigation and native builds
41
+
42
+ | Setup | Action |
43
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
44
+ | Theme's `iosTransitionAnimation` | Keep it; native transitions are already coordinated. |
45
+ | Ionic default or custom animation | Configure the [transition adapter](migration.md#native-ui-transition-adapter). |
46
+ | Plugin installed or updated | Run `npx cap sync ios`, then rebuild the native app. |
47
+ | SPM or CocoaPods | Keep the existing dependency manager; both are supported. |
48
+ | Optional CocoaPods-to-SPM migration | Run `npx cap spm-migration-assistant`, then link the generated `CapApp-SPM` package to the app target in Xcode. |
49
+ | Ordinary native glass | Requires Xcode 26+, Capacitor 8 and iOS 26+. |
50
+ | Web, Android, SSR or older iOS | Retain the Web implementation. |
51
+
52
+ No per-page registration, component list, native callback or Swift view controller is required.
43
53
 
44
54
  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
55
 
46
- Native appearance follows the applied class, system or always-dark theme CSS. System theme changes are synchronized while the runtime is active.
56
+ Native light/dark appearance is resolved from each source element’s computed style. See [Light and dark appearance](appearance.md) for theme inheritance, priority, live updates and the shared Vertical Bars behavior.
47
57
 
48
58
  ## Toolbar selection and grouping
49
59
 
50
- Configure ordinary and vertical toolbars independently:
60
+ Use `controls` to choose selection and grouping by placement:
51
61
 
52
62
  ```ts
53
63
  const shell = await enableNativeUI({
54
- horizontal: { buttons: false, button: true, backButton: true },
55
- vertical: { buttons: true, button: true, backButton: true },
64
+ controls: {
65
+ buttonGroup: 'verticalOnly',
66
+ button: true,
67
+ backButton: true,
68
+ },
56
69
  });
57
70
  ```
58
71
 
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
+ Here, ordinary toolbar actions project individually and eligible rail actions can group. All six control fields accept `true`, `false`, `'horizontalOnly'`, or `'verticalOnly'`; omitted fields are `true`. For all automatic controls only in the rail, use `controls: 'verticalOnly'`. See [selection defaults](configuration.md#selection-defaults) for the complete table.
72
73
 
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.
74
+ `button` selects ordinary actions inside or outside a group, including menu buttons. `buttonGroup` only groups already selected actions. With `buttonGroup: true, button: false`, no ordinary action projects without explicit selection. `backButton` independently selects back navigation, including back buttons inside `ion-buttons`; back buttons never join ordinary groups.
76
75
 
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`.
76
+ `ionic.auto` enables or disables automatic Ionic discovery. Explicit `data-native-ui` sources and extensions remain independent of both `controls` and `ionic.auto`.
82
77
 
83
78
  Restart the runtime to change these startup options.
84
79
 
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.
80
+ | Attribute | Applies in | `true` | `false` |
81
+ | ---------------------- | ----------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
82
+ | `data-horizontal-bars` | Ordinary placement | Select the action, or allow grouping on `ion-buttons`. | Exclude the action, or disable grouping on `ion-buttons`. |
83
+ | `data-vertical-bars` | Vertical rail placement | Select the action, or allow grouping on `ion-buttons`. | Exclude the action, or disable grouping on `ion-buttons`. |
84
+
85
+ On a button/back button these attributes control selection; on `ion-buttons` they control grouping only. Neither is inherited. Missing, invalid or empty values use the matching `controls` default. Vertical action opt-ins and their eligibility rules are detailed in [Vertical Bars](vertical-bars.md#explicit-action-selection-and-local-grouping).
90
86
 
91
87
  ```html
92
88
  <ion-buttons data-horizontal-bars="false" data-vertical-bars="true">
@@ -100,7 +96,7 @@ The vertical attribute allows grouping but does not select the text-only childre
100
96
  add `data-vertical-bars="true"` to each button to select them explicitly.
101
97
 
102
98
  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
99
+ the source group's glass styling, `controls: { buttonGroup: 'verticalOnly' }` also prevents a
104
100
  shared native group surface. Existing component-mode, fixed-toolbar,
105
101
  fill/color and shell opt-out requirements still apply. A horizontal clear button
106
102
  inside `ion-buttons` can project individually when grouping is disabled or the
@@ -109,8 +105,7 @@ Horizontal local overrides are re-evaluated live; vertical placement retains its
109
105
  per-page-visit rules. `data-projection="source|system"` remains a separate native
110
106
  vertical appearance setting.
111
107
 
112
- `enableVerticalControlArea()` remains vertical-only. Use `enableNativeUI()`
113
- when horizontal projection is needed, and run only one of the two runtimes.
108
+ The standalone `enableVerticalControlArea()` defaults `controls` to `'verticalOnly'`. Explicit sources and extensions remain independent. Use one startup entry per application.
114
109
 
115
110
  ## Supported markup
116
111
 
@@ -118,7 +113,7 @@ The table below describes the ordinary Native UI. Vertical Bars uses the separat
118
113
 
119
114
  | Ionic component | Supported appearance and placement | Native rendering |
120
115
  | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
121
- | `ion-button` | `fill="default"`, or clear inside an ungrouped `ion-buttons`, in a fixed header/footer toolbar | Glass `UIButton` |
116
+ | `ion-button` | `fill="default"`, or clear inside an ungrouped `ion-buttons`, in a fixed header/footer toolbar | Glass `UIButton` |
122
117
  | `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
118
  | `ion-back-button` | Standard icon and color in a fixed header/footer toolbar | Glass `UIButton`, using the resolved Ionic label/icon |
124
119
  | `ion-menu-button` | Fixed toolbar, inside the theme glass `ion-buttons` | Glass `UIButton`; original Ionic menu toggle |
@@ -128,6 +123,8 @@ The table below describes the ordinary Native UI. Vertical Bars uses the separat
128
123
 
129
124
  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
125
 
126
+ ### Opt out of native projection
127
+
131
128
  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
129
 
133
130
  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).
@@ -138,13 +135,60 @@ Only the exact value `disabled` opts out; an empty or unknown value does not opt
138
135
  </ion-toolbar>
139
136
  ```
140
137
 
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.
138
+ In ordinary placement, an explicit `data-native-ui="disabled"` or `.native-ui-disabled` opt-out inside a shared surface keeps that surface and its children on the Web, including button groups, tab bars, segments and FAB lists. This is different from disabling grouping with `buttonGroup`. 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.
139
+
140
+ ### Placement requirements
141
+
142
+ Glass appearance alone is not enough. Ordinary projection requires fixed controls:
142
143
 
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
+ | Control or location | Eligibility |
145
+ | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
146
+ | Buttons, back buttons, menu-button groups and segments | Toolbar directly inside `ion-header` or `ion-footer`, with no `ion-content` ancestor around the control. |
147
+ | Buttons directly inside a header/footer, standalone toolbars, or toolbars/headers in scrolling content | Stay on the Web. |
148
+ | FAB | Must use the `ion-content` fixed slot. |
149
+ | Back button with `.ios-theme-vertical-bars` enabled | Can project outside a fixed toolbar, including routed content or a persistent app shell. Vertical Bars does not require `ios` mode. |
150
+ | Collapsed headers, opted-out controls and departed pages | Excluded. |
151
+ | Full-width foreground modal | Can participate in Vertical Bars; other overlay surfaces keep their own layout. |
144
152
 
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.
153
+ Moving a projected control to an excluded location restores its Web rendering; moving it back re-evaluates eligibility. The application chooses where to enable Vertical Bars and which Ionic component mode to use.
146
154
 
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.
155
+ ### Tab layout
156
+
157
+ | Source layout | Result |
158
+ | -------------------------------------------------------------------- | --------------------------------------------------------------- |
159
+ | Equal-width items with the default `layout="icon-top"` | Native stacked icon/label layout, including iPad and landscape. |
160
+ | `icon-start`, `icon-end`, `icon-bottom`, `icon-hide` or `label-hide` | Entire tab bar stays on the Web. |
161
+ | Unequal item widths | Entire tab bar stays on the Web. |
162
+ | Start, center or end placement | Follows the source bar, including RTL. |
163
+ | Directional `ion-icon` artwork | Preserves its rendered RTL flip. |
164
+
165
+ The native bar uses local compact horizontal and regular vertical size classes without changing the app's size class. Label size and weight follow the Web snapshot.
166
+
167
+ ### Ordinary button grouping
168
+
169
+ These rules apply outside Vertical Bars:
170
+
171
+ | Source or setting | Result |
172
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
173
+ | Clear button inside `ion-buttons` | Can project individually. |
174
+ | Standalone clear, solid or outline button | Stays on the Web. |
175
+ | Grouping enabled with two or more eligible clear buttons | One native surface with separate actions. Menu buttons can join. |
176
+ | Single menu button | Uses its parent group as the surface. |
177
+ | Grouping disabled, mixed fills or unsupported group children | Eligible actions can still project individually. Explicit subtree opt-outs follow the shared-surface rules above. |
178
+ | Back button | Never joins an ordinary action group. |
179
+
180
+ ### Unsupported content
181
+
182
+ The following remain on the Web; the plugin does not translate arbitrary application CSS into UIKit styles:
183
+
184
+ | Area | Unsupported configuration |
185
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- |
186
+ | Buttons | Custom button colors or custom back icons/colors. |
187
+ | Containers | Collapsing headers, toolbars inside scrolling content, and ordinary modal content. See the separate Vertical Bars modal policy below. |
188
+ | Segments | Scrollable/expanded segments and segment-view integration. |
189
+ | Content | Complex slots and unsupported SVG features. |
190
+
191
+ ### Backgrounds and fallback handoffs
148
192
 
149
193
  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
194
 
@@ -154,14 +198,53 @@ When an existing control becomes unsupported, its Web source is painted before i
154
198
 
155
199
  ## State and events
156
200
 
201
+ ### Menu actions
202
+
157
203
  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
204
 
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.
205
+ ### Tab geometry and state updates
206
+
207
+ The DOM owns labels, SVG content, placement, selected values and application behavior. `ion-tab-bar` itself is the placement reference; UIKit owns internal margins and platter sizing.
208
+
209
+ | Source or viewport | Native behavior |
210
+ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
211
+ | Source bar rectangle | Sizing proposal and placement anchor, accounting for UIKit's larger outer frame. |
212
+ | `tab-bar-position-start`, `tab-bar-position-center`, `tab-bar-position-end` | Horizontal anchor, including RTL; class changes reconcile automatically. |
213
+ | `slot="bottom"` / `slot="top"` | Retains the corresponding bottom/top edge. |
214
+ | Supported stacked layout and label typography | Follow the Web snapshot. UIKit can cap content width even with fill positioning. |
215
+ | Badge or title updates | Retain native item identities and remeasure at the same anchor. |
216
+ | Different native width or height | Does not reject projection; icons are not stretched to force CSS item widths. |
217
+ | Unrecognized native layout | Restores Web rendering. |
218
+
219
+ The Web theme's standard appearance targets are:
220
+
221
+ | Measurement | Target |
222
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
223
+ | Viewport at least 768px wide, 2 / 3 / 4 / 5 tabs | Platter widths near 188 / 274 / 336 / 414pt respectively. Smaller viewports retain phone sizing. |
224
+ | Bar height | 62pt |
225
+ | Selection height / inset | 54pt / 4pt |
226
+ | Adjacent buttons | Overlap like UIKit controls. |
227
+
228
+ These targets do not guarantee identical custom fonts, icons or labels; UIKit determines intrinsic width. Icon and label alignment is checked against simulator screenshots. The plugin measures the native tab subtree without private class names or fixed inset corrections.
229
+
230
+ The runtime rechecks:
231
+
232
+ - Structural changes, affected shadow roots and sizing.
233
+ - Page lifecycle events and overlays.
234
+ - Ancestor `display: none`, `hidden`, theme classes, component removal and disabled changes.
235
+
236
+ Native touch events are checked against the current DOM and revision before clicking the original Ionic element.
237
+
238
+ ### Forms and segment values
160
239
 
161
240
  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
241
 
242
+ ### Text and artwork
243
+
163
244
  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
245
 
246
+ ### Touch, badges and accessibility
247
+
165
248
  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
249
 
167
250
  ## Searchable tabs
@@ -178,7 +261,16 @@ Opening search preserves the selected Ionic tab and does not automatically show
178
261
 
179
262
  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
263
 
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.
264
+ ### Supported search configuration
265
+
266
+ | Area | Native support | Remains on the Web |
267
+ | -------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
268
+ | Search group | Registered searchable tabs with an iOS-mode glass searchbar. | General standalone searchbars and classic searchbars. |
269
+ | Keyboard and text settings | Standard search keyboard and default autocorrection/capitalization. | Custom input modes, return-key hints, min/max length, autocomplete, autocorrection or spellcheck settings. |
270
+ | Clear/cancel controls | Default clear control, no internal cancel button. | Custom clear icons or an internal cancel button. |
271
+ | State and focus | Synchronizes `disabled`, `placeholder`, `value` and `setFocus()`. | Unsupported group configurations use Web rendering. |
272
+
273
+ ### Search lifetime and recovery
182
274
 
183
275
  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
276
 
@@ -188,15 +280,28 @@ While enabled, Native UI suppresses the WebView’s top scroll-edge effect becau
188
280
 
189
281
  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
282
 
191
- ## Transitions and recovery
283
+ ## FAB behavior
192
284
 
193
285
  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
286
 
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.
287
+ ### FAB eligibility
288
+
289
+ One unsupported child keeps the whole FAB on the Web, even when its list is closed.
290
+
291
+ | Area | Supported | Remains on the Web |
292
+ | ---------- | ----------------------------------------------------------------- | --------------------------------------------------------------- |
293
+ | Appearance | Standard circular glass. | Colored solid buttons or custom host backgrounds/shapes/motion. |
294
+ | Content | Text and resolved static SVG/`ion-icon`, including RTL mirroring. | Unsupported artwork. |
295
+ | Actions | Original click handlers. | Submit/reset or `href` FABs. |
296
+ | Placement | `slot="fixed"` on a direct child of `ion-content`. | Non-fixed placement or `slot="fixed"` on a page sibling. |
297
+
298
+ For example, the demo's red-background `floating-action-button-fixed` page retains its Web appearance.
196
299
 
197
300
  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
301
 
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.
302
+ ## Transitions and recovery
303
+
304
+ 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](ionic-vertical-bars.md#3-connect-your-navigation-animation) for the setup.
200
305
 
201
306
  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
307
 
@@ -204,15 +309,15 @@ Tab switches skip the Web/native crossfade so a retiring UIKit snapshot cannot l
204
309
 
205
310
  Standard Ionic overlays suspend ordinary native projection until dismissal. Vertical Bars use a separate policy:
206
311
 
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. |
312
+ | Surface or condition | Vertical Bars behavior |
313
+ | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
314
+ | Web toast, ordinary popover or legacy picker overlay | Keep the native rail visible, updated and interactive. |
315
+ | Popover opened from a Vertical Bars button, rendered on Web | Return the rail to Web projection until dismissal, including native rejection or preparation failure. |
316
+ | Alert, action sheet or loading | Restore Web rendering so the overlay and its dimming backdrop cover the controls correctly. |
317
+ | Menu | Keep the existing native rail behavior. |
318
+ | Full-width foreground modal | Project the modal's controls instead of the covered page's controls. |
319
+ | Modal that cannot use Vertical Bars | Restore Web rendering. |
320
+ | Explicit suspension, unavailable native layout, hidden document or unsupported keyboard/viewport geometry | Preserve the existing fallback behavior. |
216
321
 
217
322
  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
323
 
@@ -220,7 +325,7 @@ During retirement, the source is restored and allowed to paint before its native
220
325
 
221
326
  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
327
 
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.
328
+ Failures or timeouts in required startup bridge calls return a Web fallback handle. Optional WebView metrics may fail without disabling native controls. `destroy()` waits 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
329
 
225
330
  For diagnostics or application teardown:
226
331
 
@@ -233,7 +338,7 @@ await shell.destroy(); // restore DOM, remove native controls and release listen
233
338
  Projection is enabled by calling the startup API. Partial settings override defaults:
234
339
 
235
340
  ```ts
236
- const shell = await enableNativeUI({ ionic: { auto: { tabs: false } } }); // automatic Ionic projection except tabs
341
+ const shell = await enableNativeUI({ controls: { tabs: false } }); // automatic Ionic projection except tabs
237
342
  await shell.disableVerticalControlArea(); // retain ordinary native controls
238
343
  await shell.enableVerticalControlArea();
239
344
  await shell.destroy(); // stop all projection
@@ -243,13 +348,13 @@ See [all configuration options](configuration.md) for defaults, rail-only usage
243
348
 
244
349
  ### Popovers anchored to Vertical Bars
245
350
 
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.
351
+ **Native popover presentation is Vertical Bars only.** It requires an accepted native Vertical Bars `ion-button` anchor; enabling `ionic.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
352
 
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"`:
353
+ Enable `ionic.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
354
 
250
355
  ```ts
251
356
  await enableNativeUI({
252
- verticalBars: { popover: true },
357
+ ionic: { verticalBars: { popover: true } },
253
358
  });
254
359
  ```
255
360
 
@@ -263,7 +368,21 @@ Only one popover is relayed at a time. A nested popover or modal created in the
263
368
 
264
369
  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
370
 
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.
371
+ #### Popover fallback conditions
372
+
373
+ The relay copies document and popover shadow styles and preserves content border spacing. Before presentation, it compares selected computed layout and appearance properties at the source dimensions.
374
+
375
+ | Condition | Result |
376
+ | --------------------------------------------------------- | ------------------------------ |
377
+ | Detected style/layout differences | Restores the live Web popover. |
378
+ | Unsupported ancestor visual effects or size containers | Restores the live Web popover. |
379
+ | Content exceeds 1,000 elements | Restores the live Web popover. |
380
+ | Mirrored ancestor attributes or source head styles change | Restores Web rendering. |
381
+ | Preparation fails or the native anchor is unavailable | Restores the Web popover. |
382
+
383
+ A rail-button popover falling back to Web also returns the rail to Web projection until dismissal. This compatibility check does not guarantee arbitrary CSS (including pseudo-elements and dynamic CSSOM changes), document-bound queries, or full VoiceOver/hardware-keyboard parity.
384
+
385
+ ### Custom overlay suspension
267
386
 
268
387
  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
388
 
@@ -282,19 +401,19 @@ Suspensions are nestable and `resume()` is idempotent. Native projection resumes
282
401
 
283
402
  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
403
 
285
- ## Support iPhone Duo (preview)
404
+ ## Support iPhone Duo
286
405
 
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.
406
+ iPhone Duo support, including standalone Vertical Bars, is stable starting with version 2.0.0.
288
407
 
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.
408
+ 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({ controls: 'verticalOnly' })` for this use case; its default selection targets 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
409
 
291
410
  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
411
 
293
412
  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
413
 
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.
414
+ **Native button appearance:** Native vertical buttons default 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 priority and fill rules.
296
415
 
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).
416
+ For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](ionic-vertical-bars.md).
298
417
 
299
418
  ## Native UI API
300
419