Skip to content
WebView Native UIdocs

Native UI without Ionic

Add native buttons to an existing Web app without Ionic or theme CSS. Keep labels, state, and click handlers on ordinary HTML elements. Standard buttons require no custom Swift registration.

Use a Capacitor 8 app with an iOS project, Xcode 26+, and an iOS 26+ device or simulator. Browsers and Android retain Web controls. For custom Android native content, use Kotlin/Compose extensions.

Start with how projection works: the HTML element remains the source while native controls or Web proxies provide its presentation.

Vertical Bars is part of the standard setup. Follow the device placement setup, alongside steps 1–4 below. Reuse the step 3 runtime without a fixed placement preference. Remove explicit markers from buttons you want to leave on the Web. You do not need Ionic, @rdlabo/ionic-theme-ios27, or vertical-bars.css.

For Vertical Bars in an app using ion-tabs, ion-toolbar or other Ionic components with a custom theme, use the Ionic Vertical Bars setup instead. The framework (Angular, React or Vue) alone does not determine which adapter to use.

1. Install the plugin

Capacitor Foldable is our standard integration for device state. Its placement and posture APIs let your app control how the layout adapts; installing it is followed by connecting its initial values and change events.

Keep your existing Capacitor installation and add Native UI and Foldable together:

npm install @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable

The /core entry requires @capacitor/core but no Ionic dependencies. If this is currently a browser-only application, configure its Capacitor 8 iOS project before running native controls.

2. Mark source controls

<main id="app">
  <button type="button" data-native-ui="button" data-native-ui-placement="element">Add</button>
  <button type="button" data-native-ui="back-button" data-native-ui-placement="rail">Back</button>
</main>
<aside id="rail" aria-label="Navigation"></aside>
Attribute Value Effect
data-native-ui button Project a standard action button.
data-native-ui back-button Project a standard back button. The source click handler owns navigation; the plugin never calls history.back().
data-native-ui button-submit Project a button with the prominent glass emphasis; the tint derives from the resolved foreground color.
data-native-ui disabled Exclude the element and its subtree from projection.
data-native-ui-placement element (default) Use the source element's rectangle.
data-native-ui-placement rail Request Vertical Bars; use the optional Web rail host when native rail presentation is unavailable.

Apply button and back-button to <button>, explicitly marked ion-button, or elements exposing role="button" and click behavior. Unmarked HTML is never inferred to be a control. data-native-ui-placement chooses placement for a declared source; it does not opt an unmarked HTML element into projection.

Keep the control's data and behavior on its source:

Source property Projection behavior
Accessible name Resolves from aria-labelledby, then aria-label, then text.
Artwork Uses supported static SVG.
Disabled state Respects native disabled, a disabled fieldset, and aria-disabled="true".
Visibility and scope Hidden, removed, excluded or out-of-scope sources are retired; stale actions are rejected.
Application handlers Remain on the original element.

Native and Web-proxy actions click the original element. An ordinary <button type="submit"> therefore keeps its original form behavior. Use type="button" for actions that must not submit. The native bridge does not preserve browser user-activation privileges for APIs such as popup opening.

3. Start after mounting the controls

import { enableNativeUI } from '@rdlabo/capacitor-native-ui/core';

const shell = await enableNativeUI({
  root: document.querySelector<HTMLElement>('#app')!,
});

Call once after the source root exists. The plugin does not add page padding or safe-area rules; retain your application's CSS. For Angular, React, or Vue, use the application startup and device layout with the /core import and your own layout callback. For animated navigation, use suspension through completion and cancellation.

Device placement for Vertical Bars

Foldable is already included in the standard installation in step 1. Connect it to your application layout as follows.

Reuse the runtime from step 3 without verticalBars.placement. Native UI then follows the device-reported edge; a fixed edge: 'trailing' would constrain it and can reject a device's leading rail.

Follow Foldable integration:

  1. Use Foldable 8.5 or later within version 8. At application startup, subscribe to layoutChange before the initial read, and resolve each report with resolveVerticalBarsLayout(Foldable) from @rdlabo/capacitor-native-ui/integration.
  2. Apply the resolver's edge, inset and toolbarTop through your application's existing layout state and CSS. Reserve the inset on the reported logical side and position the rail toolbar at the reported origin. Preserve your own defaults for undefined fields; do not invent device dimensions. A display change may alter toolbarTop without changing edge or inset, which is why the standard connection uses layoutChange.
  3. On a null or unavailable edge, restore ordinary spacing and call shell.disableVerticalControlArea(); when an edge returns, call shell.enableVerticalControlArea(). Serialize these changes with your application's layout updates.

The resolver supplies values only: it neither subscribes nor writes CSS nor starts Native UI. Your application owns Web layout; Native UI owns control projection. No theme helper or theme stylesheet is required, and no device policy belongs in individual page business handlers.

Keep the explicit source markers from step 2; they declare the controls and their requested placement. To project only Back, remove data-native-ui="button" from Add. controls does not disable explicit declarations. Without a Web rail host, unavailable native rail controls stay at their original Web positions. The theme package is not required.

4. Build and run on iOS

For CocoaPods projects, first complete the native project setup. SwiftPM resolves Engine automatically.

Build your Web app, run npx cap sync ios, and launch the iOS project from Xcode. With the step 3 configuration, Add renders at its source position and invokes its original click handler. Back requests the device-reported native rail when available; the device connection from step 3 keeps Web spacing in sync. In a browser, the same button remains an ordinary Web button. Unmarked HTML remains unchanged.

If a button stays on the Web on supported iOS, check that it is visible within the configured root, has an accessible name, and that the direct plugin dependency was synced and the iOS app rebuilt.

In an app that also uses Ionic, set ionic: { auto: false } to project only explicit sources and registered extensions. See selective Ionic projection.

Custom overlays

Before presenting a custom modal that makes the background non-interactive, call await shell.suspend({ covered: true }) and resume the returned lease after dismissal. Prepared native images preserve the background appearance without imposing native dimensions on your HTML. Rail images are anchored to their proxies inside your webHost, so scrolling the original source does not move them. Their image width and height come from Native even when your source and proxy buttons have different CSS sizes. Call shell.suspend() without options when controls must remain interactive on the Web. See custom overlay suspension for lifecycle and fallback behavior.

Match the HTML theme

For a custom HTML theme, declare color-scheme: light or color-scheme: dark in the same CSS that selects your Web palette. Background colors alone do not select native appearance. See Light and dark appearance for a manual-toggle example, precedence when Ionic/theme variables are present, and shared rail behavior.

Applications that replace the source root

Only if your application unmounts or replaces this root, retain the Foldable listener handle and remove it before calling await shell.destroy(). See runtime configuration when replacing a runtime.

Optional browser rail preview

For a browser-only layout preview, temporarily use this fixed Web rail instead of the startup call in step 3 and the device subscription. Do not copy the fixed placement into the standard device setup. Call once after the source root and host are mounted, and call await shell.destroy() when their owner is disposed:

import { enableNativeUI } from '@rdlabo/capacitor-native-ui/core';

const shell = await enableNativeUI({
  root: document.querySelector<HTMLElement>('#app')!,
  verticalBars: {
    placement: {
      edge: 'trailing',
      webHost: document.querySelector<HTMLElement>('#rail')!,
    },
  },
});

The plugin uses device-reported rail availability and resolves leading/trailing through the document direction. A placement preference never forces an unsupported system rail. Native rejection restores Web presentation. With webHost, explicitly rail-marked HTML controls have Web proxies when native projection is unavailable. Without it, their originals remain visible. The application styles and positions the host and reserves any content spacing. The host must be outside the source root.

Explicit markup chooses placement: Back goes to the preview rail and Add stays at its original position. Disabling rail projection restores original rail controls and removes Web proxies without destroying ordinary native projection. Before building for a device, restore the step 3 runtime and the Foldable subscription above.

Currently the HTML adapter supports buttons and back buttons. Arbitrary HTML tabs, segments, searchbars and FAB structures are not translated. Ionic adapters continue to support their existing controls. Custom element extensions work with the same scoped root; the existing tab-accessory extension placement still requires an Ionic tab-bar owner.

Example

The plain HTML example demonstrates Add, routerless Home/Details/Back navigation, optional Web rail projection and cleanup. It uses no Ionic or theme CSS.