Configuration
For installation, use the minimal standard setup: enableNativeUI() includes ordinary controls and Vertical Bars by default. This page is the settings reference; do not copy its expanded options as installation boilerplate. Start with how projection works, then Ionic setup or HTML setup for installation.
Call enableNativeUI() once after your application's DOM is mounted. Ionic and explicitly marked HTML controls share the standard Swift renderers. The theme is optional; Web layout, safe areas and spacing belong to your app or theme.
import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
const shell = await enableNativeUI();
Without Ionic, import the same API from @rdlabo/capacitor-native-ui/core. That entry does not load Ionic's optional popover integration. See plain HTML.
Platform behavior
| Feature | Supported iOS | Android | Browser / older iOS |
|---|---|---|---|
Automatic Ionic controls (ionic.auto) |
Native | Original Web controls | Original Web controls |
| Explicit element buttons | Native | Original Web controls | Original Web controls |
| Element extensions | SwiftUI | Kotlin / Compose | Original Web content |
| Vertical Bars | Native when available; otherwise Web rail | Web rail | Web rail |
tab-accessory extensions |
Supported iOS tab host only | Original Web content | Original Web content |
Web rails require the Ionic rail layout or an application-owned HTML webHost. Settings never create a device capability.
For HTML-derived light/dark behavior, see Light and dark appearance. Appearance is configured in Web CSS, not through a startup option.
All settings
controls chooses selection and grouping defaults. verticalBars configures the rail. ionic configures Ionic discovery and appearance. Omitted fields default to true in controls, including partial objects.
const shell = await enableNativeUI({
root: document.body,
controls: {
buttonGroup: true,
button: true,
backButton: true,
tabs: true,
segment: true,
fab: true,
},
verticalBars: {
enabled: true,
// Optional: placement: { edge: 'trailing', webHost: appOwnedHost },
},
extensions: [],
ionic: {
auto: true,
verticalBars: {
buttonProjection: 'system',
buttonDefaultFill: 'glass',
popover: false,
},
},
debug: false,
});
Selection defaults
Every control accepts ControlProjection. controls itself accepts the same values as a shorthand for all six fields.
| Value | Ordinary placement | Vertical rail placement |
|---|---|---|
true (default) |
Selected | Selected |
false |
Not selected | Not selected |
'horizontalOnly' |
Selected | Not selected |
'verticalOnly' |
Not selected | Selected |
Horizontal and vertical describe control placement, not screen orientation or device model. These defaults do not move a control, provide a missing device capability, or make unsupported markup eligible.
| Control | Responsibility |
|---|---|
buttonGroup |
Group already selected actions when the group qualifies. Never selects children. |
button |
Select ordinary buttons and Ionic menu buttons. |
backButton |
Select back navigation, independently of its parent group. |
tabs |
Select tabs and their registered search presentation. |
segment |
Select supported segments. |
fab |
Select supported floating action buttons. |
Local data-horizontal-bars="true|false" and data-vertical-bars="true|false" override the matching selection default. On ion-buttons they control grouping only. Explicit data-native-ui sources are independent of these defaults. Structural exclusions and rail availability still apply. See toolbar selection and grouping.
Toolbar button correspondence
Native toolbar buttons receive presentation identities automatically. Ordinary buttons correspond by their toolbar, logical slot and order among projected buttons. Menu and Back buttons at the leading position correspond in both horizontal and vertical bars. Changing the layout axis preserves presentation identities and group order. Each surface has its own child positions, so adding or removing buttons inside one group does not change the identities of later surfaces or their buttons. Labels and click handlers may change without requiring an ID attribute. Header and footer toolbars are separate scopes.
Optionally set data-transition-id on an individual button when it should follow a particular action through a reorder within its corresponding surface:
<!-- Source page -->
<ion-button data-transition-id="edit">Edit</ion-button>
<!-- Destination page -->
<ion-button data-transition-id="edit">Done</ion-button>
An explicit key overrides the automatic position within the corresponding toolbar. It does not change HTML id, routing or click delivery. Keep the original handler on each page's source button. Blank values use automatic correspondence; duplicate explicit keys within a toolbar are treated as separate, unmatched sources. Put the attribute on each button, including children of an ion-buttons group. Child keys match within a corresponding surface; they do not move an action between different groups. To preserve a whole group through a reorder, also give its ion-buttons element a surface key. The group key identifies the shared background and child keys identify its buttons, even when only one child remains.
These identities drive the native SwiftUI Vertical Bars toolbar. With the theme's navigation animation adapter, corresponding horizontal fixed toolbar surfaces remain native and follow the Ionic page animation's progress, including interactive reversal. Geometry and content change during playback; action ownership changes only after navigation commits. A canceled swipe restores the original toolbar. Retained horizontal toolbar actions are ignored until navigation and the destination update finish.
Outgoing controls without corresponding surface identities remain native and fade out in place with page progress; cancellation reverses the fade. This opacity animation does not reproduce the system navigation bar’s blur effect. New destination controls are prepared natively before playback and grow into place, retaining the same native view when navigation completes. Single buttons and groups use the same numbered positions within each toolbar slot, counting from the logical outer edge: start from inline-start and end from inline-end. Group children follow the same rule. In LTR this means start from the left and end from the right; RTL mirrors those edges. A group at end position 0 can become a single button at end position 0; changing its child count or fill does not break correspondence or renumber the following surface. The background resizes while added or removed buttons grow or shrink. An optional data-transition-id on a surface overrides its positional identity. Moving page content, explicit runtime suspension, unsupported controls, native rejection and overlay fallback also use Web presentation. Ionic clear buttons inside ion-buttons share one glass surface, including buttons whose omitted fill resolves to clear.
Progress updates use an optional capability acknowledged by the native plugin. Older native binaries that do not acknowledge it are not sent the new bridge method: outgoing toolbar surfaces, including unmatched ones, stay visible until the completion-time handoff. Older transition adapters keep their existing behavior. Updating both the theme and native plugin enables progress synchronization.
Vertical Bars mirror the Web page hierarchy in a SwiftUI NavigationStack to use its standard push/pop toolbar animations. Web routing and each source button still determine navigation and actions; the native stack does not introduce another navigation history. Canceled navigation restores the previous path, and tab and modal pages retain their own ancestry. During an interactive swipe, Vertical Bars keep the source toolbar while the finger is down, matching the native vertical toolbar behavior. A canceled swipe leaves it unchanged. When the finger lifts and the gesture commits, the standard toolbar transition starts alongside the Web page settling into place. The projected Web toolbar sources remain hidden and native rail input is suspended until the swipe settles. Horizontal toolbars instead follow the page animation progress frame by frame. Automatic positional identities request correspondence by position, so use an explicit key when an action should follow a reorder instead.
Other settings
| Setting | Default | Meaning |
|---|---|---|
root |
document.body |
Connected source subtree. Controls outside it are not projected. |
verticalBars.enabled |
true |
Allow rail projection when available. |
verticalBars.placement |
omitted | Optional { edge: 'leading' | 'trailing', webHost?: HTMLElement }. Edge is a preference, not a capability declaration. |
extensions |
[] |
Experimental. App-specific SwiftUI / Compose extensions. Independent of controls and ionic.auto; the app decides when its reader returns content. |
ionic.auto |
true |
Enable automatic Ionic discovery. false disables it, including local layout opt-ins. Explicit data-native-ui sources and extensions remain independent. |
ionic.verticalBars.buttonProjection |
'system' |
'system' uses native styling; 'source' projects supported Ionic styling. Local appearance overrides take precedence. |
ionic.verticalBars.buttonDefaultFill |
'glass' |
'glass' or 'solid' for source-style standalone Ionic buttons without explicit fill. Explicit fills and the clear default inside groups take precedence. |
ionic.verticalBars.popover |
false |
Opt into Ionic popovers anchored to native rail buttons. Requires the Ionic entry and its optional peers. |
debug |
false |
Log source elements and fallback reasons to the developer console. |
HTML fallback rail host
webHost is an optional, application-styled HTML fallback rail. Its requirements are:
- Connected to the same document as
root. - Outside
root; neither element may contain the other. - Styled and positioned by the application, including any reserved content spacing.
Use a limited root, such as your <main>, with a sibling host; the default document.body cannot have such a host. Without a host, unavailable HTML rails leave original controls visible. Ionic's existing rail layout uses its own fallback adapter.
HTML currently supports buttons and back buttons. Tab, segment and FAB readers remain Ionic adapters; enabling those settings does not infer equivalent structures from arbitrary HTML.
Common configurations
// Ionic or HTML: all supported controls, rail support enabled by default.
const shell = await enableNativeUI();
// Alternative: automatic selection only in the rail.
const railOnly = await enableNativeUI({ controls: 'verticalOnly' });
// Alternative: automatically select only tabs.
const tabsOnly = await enableNativeUI({
controls: { buttonGroup: false, button: false, backButton: false, tabs: true, segment: false, fab: false },
});
These are alternatives: start only one shell. Repeating equivalent startup settings returns its handle; different settings require destroying the previous shell.
controls: false disables all default selection and grouping, while local attributes can still opt in. ionic: { auto: false } instead disables automatic Ionic discovery altogether. Neither disables explicit HTML sources or extensions.
verticalBars.enabled: false leaves rail controls on the Web regardless of selection. It does not affect ordinary controls. data-native-ui-placement="element" preserves an explicit control's original position, even with controls: 'verticalOnly'. For Ionic groups, put placement on the whole ion-buttons group so its shared native surface has one placement.
Select controls explicitly in a customized Ionic app
Disable automatic Ionic projection while retaining the native rail and your custom extensions:
const shell = await enableNativeUI({
root: document.querySelector<HTMLElement>('#content')!,
ionic: { auto: false },
verticalBars: {
enabled: true,
placement: {
webHost: document.querySelector<HTMLElement>('#rail')!,
},
},
extensions: [myExtension],
});
<main id="content">
<ion-button>Keep this on the Web</ion-button>
<ion-button data-native-ui="button" data-native-ui-placement="rail">Project this action</ion-button>
<button data-native-ui="button">Project at its original position</button>
</main>
<aside id="rail" aria-label="Actions"></aside>
Keep your original handlers on each source. Explicit ion-button uses the standard button reader, so supported content and styling rules still apply; the attribute does not make arbitrary custom markup supported. data-native-ui-placement="rail" requests the system rail when available. Style the optional webHost and reserve space in your app; without it, unavailable explicit rails keep their source controls visible. Unmarked Ionic controls remain Web-rendered. An automatically projected group containing an explicitly marked source stays on the Web to avoid covering that source twice.
Explicit built-in sources support individual button, ion-button, and [role="button"] elements with data-native-ui="button" or "back-button". This does not opt existing ion-back-button, ion-menu-button, button groups, tabs, segments, or FABs into their Ionic-specific readers. Keep automatic projection enabled for the corresponding kind, use a standard explicit button with your own click handler, or register an extension for custom UI.
verticalBars.enabled still governs rail availability. To leave a specific source on the Web, remove its explicit marker or use data-native-ui="disabled".
Change rail use after startup
await shell.disableVerticalControlArea();
await shell.enableVerticalControlArea();
These methods change verticalBars.enabled on this shell and preserve appearance, controls and placement. They retire existing projection before transferring ownership, without creating another shell. Calls are serialized. Calling either method after destroy() rejects.
const lease = await shell.suspend(); // temporarily restore all Web controls
await lease.resume();
await shell.destroy(); // stop the shell and restore sources
Disabling rail projection does not remove application CSS or change application-owned spacing. Update your own layout alongside this setting if needed.
Request a check after an unobserved change
shell.markForCheck();
Use this after changing a property or reader state that does not cause a DOM mutation. It schedules a check, batches repeated requests, and sends only changed native snapshots. It returns void; it does not wait for native completion or render your framework's DOM. Calls after destruction are ignored. See source checks and observation.
Understand why a source stays on the Web
Temporarily add debug: true to your existing startup options, then rebuild the Web app. Do not start a second shell to enable diagnostics:
const shell = await enableNativeUI({ debug: true, extensions: [myExtension] });
console.info(shell.getStatus());
Open the WebView developer console and filter for [Native UI]. Diagnostics include the DOM source when available. Each reason is logged once per source during that activation, so scrolling or repeated markForCheck() calls do not flood the console. Props, action payloads and application exception messages are not logged.
| Diagnostic | What to check |
|---|---|
web-fallback |
Platform support, native plugin registration, sync and native rebuild. |
unregistered-extension |
Native registration name matches the DOM definition and runs before startup. Newly inserted sources are checked too. |
reader-error / invalid-props |
Reader implementation, JSON-only values and the 64K size limit. |
unsupported-placement / missing-tab-owner |
Platform and owning native tab host for a tab accessory. |
source-hidden / source-clipped / source-focused |
Visibility, clipping/transformed ancestors, or a focused Web descendant. |
reader-opt-out |
Your reader returned null. |
unsupported-control |
Supported markup, accessible name, fixed placement and styling. |
native-rejected |
Native props decoding, placement or device support. The bridge reports rejection, not the native exception. |
getStatus() summarizes the shell; it is not a per-element diagnostic report. Normal navigation, suspension and unsupported content can intentionally retain Web presentation. Turn debug off after investigating; it does not change projection eligibility.
Existing applications
The standalone enableVerticalControlArea() defaults selection to 'verticalOnly'. Its setPlacement() helper is a deprecated Web CSS adapter; use the theme's setVerticalBarsLayout({ edge, inset }) for Web layout. Native availability comes from the plugin's device metrics.
See upgrading from 1.2.1 for package separation, removed options and changed partial-control defaults.
Native project setup
SwiftPM resolves Engine automatically. For an existing CocoaPods app, install the Engine version required by your installed Native UI package:
npm install "@webview-native-ui/engine@$(node -p "require('./node_modules/@rdlabo/capacitor-native-ui/package.json').dependencies['@webview-native-ui/engine']")"
Add Engine to the existing application target in ios/App/Podfile, outside the generated def capacitor_pods block:
target 'App' do
capacitor_pods
pod 'WebViewNativeUIEngine', :path => '../../node_modules/@webview-native-ui/engine'
# Keep your other application pods here.
end
Adjust the path for your project layout and repeat version alignment when upgrading Native UI. This is CocoaPods compatibility setup, not application UI logic.