<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Masahiko Sakakibara</title>
    <description>The latest articles on DEV Community by Masahiko Sakakibara (@rdlabo).</description>
    <link>https://dev.to/rdlabo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1859869%2F8bec28f2-c751-4640-a79e-15b015aab370.jpeg</url>
      <title>DEV Community: Masahiko Sakakibara</title>
      <link>https://dev.to/rdlabo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rdlabo"/>
    <language>en</language>
    <item>
      <title>I Didn’t Want “Built with Web Tech” to Mean Compromise [Capacitor/Cordova]</title>
      <dc:creator>Masahiko Sakakibara</dc:creator>
      <pubDate>Tue, 03 Nov 2026 07:32:24 +0000</pubDate>
      <link>https://dev.to/rdlabo/i-didnt-want-built-with-web-tech-to-mean-compromise-capacitorcordova-4b96</link>
      <guid>https://dev.to/rdlabo/i-didnt-want-built-with-web-tech-to-mean-compromise-capacitorcordova-4b96</guid>
      <description>&lt;p&gt;HTML, CSS, JavaScript. The same technologies you use for websites and web apps can also power iOS and Android apps. Capacitor and Cordova let you run those web interfaces inside a mobile app, with plugins providing access to features such as push notifications and Face ID.&lt;/p&gt;

&lt;p&gt;The component that displays the web interface inside the app is called a WebView. Apps built this way are often called WebView or hybrid apps. They aren't just small experiments: in Japan, examples include AEON's iAEON shopping app and the Sumitomo Mitsui Banking Corporation app.&lt;/p&gt;

&lt;p&gt;There are challenges, though. Recreating native UI with web technologies has limits. Liquid Glass, introduced in iOS 26, is a good example: the glass changes shape under your finger and refracts the content behind it.&lt;/p&gt;

&lt;p&gt;Then there are ads. Google AdMob is a common choice for mobile apps, but making a native ad scroll between posts in a web feed is a different problem from placing a banner on the screen. The web content and the native UI live in separate rendering layers.&lt;/p&gt;

&lt;p&gt;For almost ten years, I thought of these as limitations of WebView apps. But after questioning the requirements and experimenting, I finally found a way to make native UI and web content coexist without disrupting the experience!&lt;/p&gt;

&lt;h2&gt;
  
  
  Native UI alongside web content
&lt;/h2&gt;

&lt;p&gt;Close this web-based sheet, and the iOS tab bar appears underneath. You can then interact with it, including a long press. The tab bar is native UI rendered by UIKit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvsg61yhm85pcev1kus9c.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvsg61yhm85pcev1kus9c.gif" alt="A web sheet closes over a UIKit tab bar. A long press then interacts with the native tab bar." width="560" height="975"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Web content and native UI, behaving naturally on the same screen. I honestly used to think this wasn't possible!&lt;/p&gt;

&lt;h3&gt;
  
  
  An iOS tab bar that controls the web app
&lt;/h3&gt;

&lt;p&gt;Compare a long press on a web recreation with the native tab bar. The curvature of the glass and the distortion of the photo behind it are different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjfi0xmcsphyw11q5eym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjfi0xmcsphyw11q5eym.png" alt="A long press on a web recreation and a native tab bar, with the glass enlarged for comparison." width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui" rel="noopener noreferrer"&gt;@rdlabo/capacitor-native-ui&lt;/a&gt; to use this native UI alongside web content. It reads supported Ionic components and renders their native counterparts with UIKit and SwiftUI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhkew0n8ly8uqhz88cvtp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhkew0n8ly8uqhz88cvtp.png" alt="Comic 1, part 1: iOS renders the tab bar. What happens to the web app when it is tapped?" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs8jaaevjq7f70ws4xuy0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs8jaaevjq7f70ws4xuy0.png" alt="Comic 1, part 2: The interaction is passed to the web app. Web content and iOS UI work as one app." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Labels and selection state can still be managed on the web side. Ionic isn't required, either: in a plain HTML app, you can explicitly mark buttons and back buttons for native rendering.&lt;/p&gt;

&lt;p&gt;I've also released a &lt;a href="https://github.com/webview-native-ui/cordova-plugin" rel="noopener noreferrer"&gt;Cordova plugin&lt;/a&gt;. It shares the native rendering implementation, so Cordova apps can use it too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Native ads that scroll with the posts
&lt;/h3&gt;

&lt;p&gt;Navigation wasn't the only place I wanted native UI. I wanted ads between posts, scrolling with the feed, as they do on X. The posts and photos are web content; the ads between them are rendered by the Google Mobile Ads SDK.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7hmiozfa42zm9t92yqtz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7hmiozfa42zm9t92yqtz.gif" alt="Native ads scroll with web posts on iOS and Android, disappearing behind the fixed header." width="560" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Both platforms are showing test ads.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After tuning the behavior separately on iOS and Android, I made this available through &lt;a href="https://github.com/capacitor-community/admob" rel="noopener noreferrer"&gt;@capacitor-community/admob&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41vukmusi3lyh9oledfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41vukmusi3lyh9oledfx.png" alt="Comic 2, part 1: Web posts and native ads move together in the feed." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmcesqn2l0mbdinqm0e3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmcesqn2l0mbdinqm0e3.png" alt="Comic 2, part 2: The ad disappears behind the header. How do the two rendering systems stay in sync?" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When you can't interact with it, could an image do?
&lt;/h2&gt;

&lt;p&gt;The hardest part of both features was the boundary between the web and native rendering layers.&lt;/p&gt;

&lt;p&gt;CSS &lt;code&gt;z-index&lt;/code&gt; only controls stacking inside the WebView. A web sheet cannot cover a native view placed above the WebView itself. Scrolling has the same problem: moving the web content doesn't automatically move the native view with it.&lt;/p&gt;

&lt;p&gt;They are separate rendering systems. Of course they behave differently. But to the person using the app, it's all one screen.&lt;/p&gt;

&lt;p&gt;So I reconsidered the requirement: when does the UI actually need to remain a live native view?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9jn5m5ssobytv0pzj5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9jn5m5ssobytv0pzj5d.png" alt="Comic 3, part 1: Separate layers prevent a web sheet from covering native UI. But the tab bar cannot be used while the blocking sheet is open." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fqlf5pulogpacikugjn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fqlf5pulogpacikugjn.png" alt="Comic 3, part 2: Put an image of the tab bar in the web layer temporarily, then restore the native view when the sheet closes." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The native UI itself doesn't need to enter the DOM. Only its appearance does. That realization was a thrill. I call the approach “Projection.”&lt;/p&gt;

&lt;p&gt;But taking a snapshot isn't the whole solution. Hide the native view first, and you get a blank frame before its image appears. Show the image first, and the translucent glass appears twice, layered over itself.&lt;/p&gt;

&lt;p&gt;Neither works.&lt;/p&gt;

&lt;p&gt;So I prepare the snapshot and get it ready for display on the web side before switching. When hiding the native view, I temporarily display the same image on the native side to bridge the handoff. Once the web side displays its copy, I remove the temporary native-side image in step with rendering. The appearance stays the same; only the place rendering it changes.&lt;/p&gt;

&lt;p&gt;Tuning that handoff frame by frame produced the behavior in the opening demo. Just watching it, you can't tell when the tab bar becomes an image.&lt;/p&gt;

&lt;p&gt;It feels like magic!&lt;/p&gt;

&lt;p&gt;There are limits: a snapshot cannot preserve interaction or video playback. Scrolling also calls for different strategies. In supported iOS scroll regions, the live native view follows the content. For Android ads that can be captured, an image is projected while scrolling.&lt;/p&gt;

&lt;p&gt;I extracted the shared coordination into &lt;a href="https://github.com/webview-native-ui/engine" rel="noopener noreferrer"&gt;WebView Native UI Engine&lt;/a&gt;, so other plugin authors can use it. My &lt;a href="https://dev.to/rdlabo/i-wanted-real-native-ui-in-my-webview-so-i-built-projection-capacitorcordova-3mdh"&gt;Projection article&lt;/a&gt; goes deeper into preparing the snapshot and timing the handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting the app to iPhone Duo
&lt;/h2&gt;

&lt;p&gt;Using native UI also gave me a way to support iPhone Duo!&lt;/p&gt;

&lt;p&gt;On Duo's outer display and its inner display in landscape, back buttons and tab bars move to the sides. This arrangement is called Vertical Bars. Moving navigation to the sides gives the same Ionic app more vertical room for its content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3pm494i54i830zaevhl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3pm494i54i830zaevhl.gif" alt="An Ionic app switches to Vertical Bars, with native navigation at the sides." width="720" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This recording enables the vertical layout using a switch inside the demo app.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Moving the bars is only part of the work. The web content needs to leave room for them, too. The background can fill the screen, while cards and lists stay clear of the bars. Safe areas become a question of how the content and controls fit together, not just how much padding to put at the top and bottom.&lt;/p&gt;

&lt;p&gt;Vertical Bars follow iOS conventions for width, button order, grouping, and animation. Other apps follow those same conventions, which makes small differences in a web recreation easier to notice. Matching the transitions and the changing shape of the glass is particularly tricky. This is where I want to use native UI.&lt;/p&gt;

&lt;p&gt;A plain HTML app can use it too. In this example, pressing the native back button on the right runs the existing web-side handler and returns to the list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzi46c9zkhl12q2096037.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzi46c9zkhl12q2096037.gif" alt="A plain HTML Capacitor app returns from a detail view to its list using the native side back button." width="720" height="1048"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Outside Ionic, this integration supports explicitly marked buttons and back buttons. It does not automatically convert arbitrary HTML tab bars.&lt;/p&gt;

&lt;p&gt;Device layout information comes from Erkam Yaman's &lt;a href="https://github.com/erkamyaman/capacitor-foldable" rel="noopener noreferrer"&gt;Capacitor Foldable&lt;/a&gt;. I use that information to align the web layout with the native controls. Thank you, Erkam!&lt;/p&gt;

&lt;p&gt;You still need to initialize the plugins and connect the layout updates. But you don't need a separate app UI just for Duo. The documentation has guides for &lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui/docs/ionic-vertical-bars" rel="noopener noreferrer"&gt;Ionic Vertical Bars&lt;/a&gt; and &lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui/docs/framework-independent" rel="noopener noreferrer"&gt;framework-independent integration&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More room for what we can build with the web
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzj6ww8zaiu1scr63t4sf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzj6ww8zaiu1scr63t4sf.png" alt="Comic 4, part 1: The developer wants an app that feels at home on iPhone Duo. Native iOS navigation is an option." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7iccrr3e4ym5s7rgh5iv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7iccrr3e4ym5s7rgh5iv.png" alt="Comic 4, part 2: The content stays web-based while navigation follows the device's conventions. The developer can pursue both the interface and the experience they want." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the plugins and libraries described here are open source under the MIT license:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui" rel="noopener noreferrer"&gt;Native UI for Capacitor: @rdlabo/capacitor-native-ui&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/webview-native-ui/cordova-plugin" rel="noopener noreferrer"&gt;Native UI for Cordova: @webview-native-ui/cordova-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/capacitor-community/admob/blob/main/docs/native-ads.md" rel="noopener noreferrer"&gt;Native ads: @capacitor-community/admob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/webview-native-ui/engine" rel="noopener noreferrer"&gt;Shared coordination for plugin authors: WebView Native UI Engine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building with web technologies doesn't have to mean giving up the native capabilities your app needs. You can build the content and behavior with the web, then bring in native UI where it makes sense. That's the choice I wanted to make possible.&lt;/p&gt;

&lt;p&gt;After almost ten years of treating this as a constraint, I finally got past it. This was software development, but it felt like inventing something. I'm proud of this one!&lt;/p&gt;

&lt;p&gt;If this made you think, “Wait, a web-based app can do that?”, I'm happy. If it gives you something you've wanted for your own app, even better.&lt;/p&gt;

&lt;p&gt;What native UI have you wanted to use alongside your web content?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Adapted from my &lt;a href="https://note.com/rdlabo/n/n92ebe94e6789" rel="noopener noreferrer"&gt;Japanese article on note&lt;/a&gt;, with the comics translated into English.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>webdev</category>
      <category>mobile</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Ionic/Capacitor Apps for the iPhone Duo Era</title>
      <dc:creator>Masahiko Sakakibara</dc:creator>
      <pubDate>Mon, 02 Nov 2026 14:32:07 +0000</pubDate>
      <link>https://dev.to/rdlabo/building-ioniccapacitor-apps-for-the-iphone-duo-era-5elf</link>
      <guid>https://dev.to/rdlabo/building-ioniccapacitor-apps-for-the-iphone-duo-era-5elf</guid>
      <description>&lt;p&gt;iPhone Duo introduces Vertical Bars, moving back buttons and tab bars to the side of the screen.&lt;/p&gt;

&lt;p&gt;That changes the layout and the experience more than I expected. How much? App Store screenshot specifications now include iPhone Duo alongside the existing iPhone and iPad requirements. It seems Apple sees this as a distinct experience, too.&lt;/p&gt;

&lt;p&gt;As app developers, we need to account for it. So I built a way to adapt an existing Ionic app to Duo. Take a look:&lt;/p&gt;

&lt;p&gt;&lt;a href="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/g3pm494i54i830zaevhl.gif" class="article-body-image-wrapper"&gt;&lt;img src="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/g3pm494i54i830zaevhl.gif" alt="An Ionic app on iPhone Duo switches to Vertical Bars, then uses the native side tab bar and back button." width="720" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The switch in this demo enables the vertical layout.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's the same Ionic app, with its tab bar and back button moved to the side. These aren't HTML and CSS recreations. They're real native UI rendered by UIKit and SwiftUI.&lt;/p&gt;

&lt;p&gt;The web screens and application logic stay in place. Native UI handles the navigation controls.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://zenn.dev/rdlabo/articles/iphone-duo-cross-platform-introduction" rel="noopener noreferrer"&gt;my previous article (in Japanese)&lt;/a&gt;, I explored how Duo behaves. This time, I'll show how to bring that behavior to an existing app. This also works with Capacitor apps that don't use Ionic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Duo changes navigation and safe areas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Back buttons and tab bars move to the side
&lt;/h3&gt;

&lt;p&gt;On Duo's outer display, and on its inner display in landscape, back buttons and tab bars move into Vertical Bars at the side. Here's the same Ionic app before and after adapting to that layout.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foszbq02yqe0k8hqj8e9i.png" alt="The same Duo app with navigation buttons at the top and the tab bar at the bottom." width="480" height="660"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fptby7cj282z28f0jus98.png" alt="The same screen with native navigation controls at the side and content inset from the bars." width="480" height="660"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same Duo, same screen. Only the vertical layout has been enabled.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the right, the buttons and tab bar have moved to the side, and the content stays clear of the bars. Moving the controls also changes how the content beside them fits on screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  What belongs inside the safe area changes, too
&lt;/h3&gt;

&lt;p&gt;Safe areas need some attention here. This isn't just a matter of moving padding from the top and bottom to the left and right. Compare these two Apple explanations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1hxjojaeqqc7rowsai3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1hxjojaeqqc7rowsai3.webp" alt="Apple's examples: on the left, iPhone X list cell backgrounds extend beyond the safe area; on the right, Duo keeps entire cards and lists inset from the bars." width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: &lt;a href="https://developer.apple.com/videos/play/tech-talks/201/?time=583" rel="noopener noreferrer"&gt;Building Apps for iPhone X (9:43)&lt;/a&gt;. Right: &lt;a href="https://developer.apple.com/videos/play/tech-talks/111461/?time=415" rel="noopener noreferrer"&gt;Prepare your app for iPhone Duo (6:55)&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the earlier iPhone example on the left, the yellow cell backgrounds extend beyond the safe area, while text and controls stay inside it. In the Duo example on the right, entire cards and lists sit inset from the bars, as shown by the blue region.&lt;/p&gt;

&lt;p&gt;The background fills the screen; lists and cards stay inset. That's a change in how we apply safe areas between iPhone and iPhone Duo, and one to keep in mind when adapting an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared UI makes small differences stand out
&lt;/h2&gt;

&lt;p&gt;Vertical Bars come with many iOS-defined constraints: width, button order, grouping, and animations. You can include custom views, but they need to adapt to the fixed width and vertical layout. &lt;a href="https://developer.apple.com/videos/play/tech-talks/111462/" rel="noopener noreferrer"&gt;Apple's walkthrough&lt;/a&gt; explains those rules.&lt;/p&gt;

&lt;p&gt;Other apps follow the same rules. That makes small differences easier to notice when we recreate something similar on the web.&lt;/p&gt;

&lt;p&gt;I've tried to reproduce iOS 26/27 UI with web technologies, down to the pixel. But did I really match the way Liquid Glass changes shape and animates? That's harder to say.&lt;/p&gt;

&lt;p&gt;For UI shared across apps, like Vertical Bars, I wanted to hand that part over to real native controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make just the Vertical Bars native
&lt;/h2&gt;

&lt;p&gt;That's why I built &lt;a href="https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui" rel="noopener noreferrer"&gt;@rdlabo/capacitor-native-ui&lt;/a&gt;. It connects native UI with a WebView so they can coexist. Here, we'll use it to rearrange existing navigation buttons and tab bars as native controls for Vertical Bars.&lt;/p&gt;

&lt;p&gt;When you use the native tab bar, its actions are passed to the original web components, and your web screens switch as before. The back button works the same way. The appearance and interaction change, while the application logic behind them stays in place.&lt;/p&gt;

&lt;p&gt;We can also use the device's layout information to keep the background full-screen and the content inset from the bars. Let's add this to an existing app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add it to an existing Ionic app
&lt;/h3&gt;

&lt;p&gt;With the setup below, the plugin reads your existing &lt;code&gt;ion-tab-bar&lt;/code&gt; and &lt;code&gt;ion-back-button&lt;/code&gt; and renders their native counterparts. Those controls support Duo's Vertical Bars, so you don't need to substantially rebuild your navigation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @rdlabo/capacitor-native-ui @rdlabo/ionic-theme-ios27 @erkamyaman/capacitor-foldable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the vertical-layout CSS to your global Sass so web content stays inset from the bars. You can keep your existing theme.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@use&lt;/span&gt; &lt;span class="s1"&gt;'@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your page transitions aren't already integrated with native UI, wrap the existing transition animation with &lt;code&gt;withNativeUITransition&lt;/code&gt; and assign it to &lt;code&gt;navAnimation&lt;/code&gt; before initializing Ionic.&lt;/p&gt;

&lt;p&gt;Initialize native UI once for the app. After &lt;code&gt;ion-app&lt;/code&gt; is mounted, handle the device's layout information at the app root and start with &lt;code&gt;verticalOnly&lt;/code&gt;. Here's that part of the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Foldable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BarPlacement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@erkamyaman/capacitor-foldable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setVerticalBarsLayout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rdlabo/ionic-theme-ios27&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enableNativeUI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rdlabo/capacitor-native-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applyPlacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;verticalBarEdge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;BarPlacement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;setVerticalBarsLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verticalBarEdge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Foldable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;barPlacementChange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;applyPlacement&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;applyPlacement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Foldable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBarPlacement&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enableNativeUI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verticalOnly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verticalOnly&lt;/code&gt; limits native rendering to Vertical Bars. The placement handler reflects the device's bar placement and insets in the web layout, so existing screens adapt to both vertical and regular layouts.&lt;/p&gt;

&lt;p&gt;When the component that owns this setup is destroyed, clean up with &lt;code&gt;listener.remove()&lt;/code&gt; and &lt;code&gt;rail.destroy()&lt;/code&gt;. If you're already initializing native UI, integrate this into that existing setup.&lt;/p&gt;

&lt;p&gt;Build the web app, run &lt;code&gt;npx cap sync ios&lt;/code&gt;, then build and run the iOS app on Duo. The &lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui/docs/ionic-vertical-bars" rel="noopener noreferrer"&gt;integration guide&lt;/a&gt; includes transition examples and explains where the setup belongs in each framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add it to an app without Ionic
&lt;/h3&gt;

&lt;p&gt;If you don't use Ionic, the plugin can display native buttons in Vertical Bars based on your existing HTML buttons. Here, the native back button at the side returns to a web-based list:&lt;/p&gt;

&lt;p&gt;&lt;a href="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/zi46c9zkhl12q2096037.gif" class="article-body-image-wrapper"&gt;&lt;img src="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/zi46c9zkhl12q2096037.gif" alt="A Capacitor app without Ionic opens a detail screen, then returns to its web list using Duo's native back button on the right." width="720" height="1048"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A plain HTML app on Duo. The back button on the right returns to the web list.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Add these two packages to your existing Capacitor project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the app's root element (&lt;code&gt;#app&lt;/code&gt; here) is mounted, initialize once. This is the startup example from the documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enableNativeUI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rdlabo/capacitor-native-ui/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enableNativeUI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine this with the &lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui/docs/framework-independent#device-placement-for-vertical-bars" rel="noopener noreferrer"&gt;documented device-placement integration&lt;/a&gt;. Get the initial placement with &lt;code&gt;Foldable.getBarPlacement()&lt;/code&gt;, listen for &lt;code&gt;barPlacementChange&lt;/code&gt;, and reflect the placement and &lt;code&gt;inset&lt;/code&gt; in your existing CSS.&lt;/p&gt;

&lt;p&gt;When vertical placement is no longer active, restore the regular spacing and call &lt;code&gt;shell.disableVerticalControlArea()&lt;/code&gt;. When it returns, call &lt;code&gt;shell.enableVerticalControlArea()&lt;/code&gt;. Process spacing updates and these state changes in sequence.&lt;/p&gt;

&lt;p&gt;When the root is destroyed, remove the placement listener and call &lt;code&gt;await shell.destroy()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With initialization in place, add &lt;code&gt;data-*&lt;/code&gt; attributes to the existing HTML buttons you want to display as native UI. This marks a back button for placement in Vertical Bars. Pressing it invokes the original click handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;data-native-ui=&lt;/span&gt;&lt;span class="s"&gt;"back-button"&lt;/span&gt;
  &lt;span class="na"&gt;data-native-ui-placement=&lt;/span&gt;&lt;span class="s"&gt;"rail"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Back
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the web app, run &lt;code&gt;npx cap sync ios&lt;/code&gt;, and rebuild the iOS app. This integration supports buttons and back buttons; it doesn't automatically convert HTML tab bars. The &lt;a href="https://docs.rdlabo.dev/projects/capacitor-native-ui/docs/framework-independent#device-placement-for-vertical-bars" rel="noopener noreferrer"&gt;guide and examples&lt;/a&gt; cover the setup in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help your Capacitor app feel at home on Duo
&lt;/h2&gt;

&lt;p&gt;When someone chooses Duo, we want them to keep using their familiar apps in a way that fits the device.&lt;/p&gt;

&lt;p&gt;Keep the web screens and logic you've already built, and use real native UI for navigation. That's now an option for adapting to Duo, too.&lt;/p&gt;

&lt;p&gt;The device-placement information comes from Erkam Yaman's &lt;a href="https://github.com/erkamyaman/capacitor-foldable" rel="noopener noreferrer"&gt;Capacitor Foldable&lt;/a&gt;. Thank you, Erkam!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui" rel="noopener noreferrer"&gt;@rdlabo/capacitor-native-ui&lt;/a&gt; is open source under the MIT license. If you're planning Duo support, give it a try in your existing app!&lt;/p&gt;

&lt;p&gt;See you next time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published &lt;a href="https://zenn.dev/rdlabo/articles/ionic-theme-ios27-v2-iphone-duo" rel="noopener noreferrer"&gt;in Japanese on Zenn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>capacitor</category>
      <category>ios</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Wanted Real Native UI in My WebView. So I Built Projection. [Capacitor/Cordova]</title>
      <dc:creator>Masahiko Sakakibara</dc:creator>
      <pubDate>Sun, 01 Nov 2026 02:47:07 +0000</pubDate>
      <link>https://dev.to/rdlabo/i-wanted-real-native-ui-in-my-webview-so-i-built-projection-capacitorcordova-3mdh</link>
      <guid>https://dev.to/rdlabo/i-wanted-real-native-ui-in-my-webview-so-i-built-projection-capacitorcordova-3mdh</guid>
      <description>&lt;p&gt;When you build a WebView app with Capacitor or Cordova, you often recreate native UI on the web to make the app feel at home on the device.&lt;/p&gt;

&lt;p&gt;I've spent more than a year working on this, releasing my work as an open-source Ionic Theme that recreates native UI with web technologies.&lt;/p&gt;

&lt;p&gt;But did I really recreate Liquid Glass, introduced in iOS 26? That's harder to answer. I could get the appearance close, but the background refraction and the way it responds to touch were beyond what I could reproduce on the web.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjfi0xmcsphyw11q5eym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjfi0xmcsphyw11q5eym.png" alt="The same tab held down in the web theme and native UI, with the glass enlarged for comparison."&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Both show a long press. Look at the edges of the glass and the background showing through it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then why not use real native UI?&lt;/p&gt;

&lt;p&gt;That brought me to a different problem: making it coexist with the WebView. Web content and native UI live in separate rendering layers. I couldn't make them feel like one natural interface.&lt;/p&gt;

&lt;p&gt;Recently, I found a way. Take a look!&lt;/p&gt;

&lt;p&gt;As I slowly close this web-based sheet modal, a native tab bar emerges from underneath it.&lt;/p&gt;

&lt;p&gt;&lt;a href="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vsg61yhm85pcev1kus9c.gif" class="article-body-image-wrapper"&gt;&lt;img src="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vsg61yhm85pcev1kus9c.gif" alt="A web sheet opens over a native tab bar, slides closed, and is followed by a long press on the restored native tab bar." width="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The web modal appears above the native tab bar. Close it, and the tab bar is interactive native UI again.&lt;/p&gt;

&lt;p&gt;This tab bar isn't an HTML or CSS recreation. It's real native UI rendered by UIKit on iOS.&lt;/p&gt;

&lt;p&gt;I call this approach &lt;strong&gt;Projection&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary a WebView can't cross
&lt;/h2&gt;

&lt;p&gt;A WebView and native UI don't share a stacking order or a scrolling system.&lt;/p&gt;

&lt;p&gt;The WebView renders HTML and CSS. No matter how high you set a CSS &lt;code&gt;z-index&lt;/code&gt;, a web modal cannot cover a native view placed above the WebView itself.&lt;/p&gt;

&lt;p&gt;Scrolling has the same problem. When content inside the WebView scrolls, a native view doesn't automatically move with it. To place native ads between posts, as you see in an X feed, you need to synchronize their positions—and clip the parts that pass behind the header.&lt;/p&gt;

&lt;p&gt;They're separate rendering systems. Of course they behave differently.&lt;/p&gt;

&lt;p&gt;But to the person using the app, it's all one screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  If native UI can't go into the DOM, project its appearance
&lt;/h2&gt;

&lt;p&gt;The turning point was thinking about &lt;strong&gt;when native UI can't be interacted with&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a modal is open, you can't use the tab bar it covers. The same goes for an alert: you can't tap the components behind it.&lt;/p&gt;

&lt;p&gt;So, while that UI is unavailable for interaction, does it actually need to stay in the native layer?&lt;/p&gt;

&lt;p&gt;Turn it into an image temporarily and place it within the WebView's stacking order. When it becomes interactive again, bring back the real native UI.&lt;/p&gt;

&lt;p&gt;The same idea works for scrolling. Switch to an image when scrolling starts, then back to the real view when it stops. Web content and native UI can coexist without compromising the user experience at all.&lt;/p&gt;

&lt;p&gt;That realization was pretty exciting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The native UI itself doesn't need to be in the DOM. Only its appearance does.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the idea behind Projection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuau0kyw3c029mkkj22et.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuau0kyw3c029mkkj22et.png" alt="Three stages: live native UI, its image projected into the WebView while the sheet is open, and live native UI again."&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;In the middle, the tab bar is an image.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In practice, there are three ways to display the UI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Live Native UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The real native view, ready for interaction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Projection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An image of the native UI inside a DOM container.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Fallback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML or another fallback supplied by the plugin when an image isn't available.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of course, you can't interact with the native UI while it's projected as an image. That makes the timing of the switch—in both directions—essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  It feels like magic
&lt;/h2&gt;

&lt;p&gt;The idea was simple. Getting the handoff right was hard.&lt;/p&gt;

&lt;p&gt;Hide the native UI before showing its image, and you get a blank frame. Show the image before hiding the native UI, and the translucent UI appears twice, layered over itself.&lt;/p&gt;

&lt;p&gt;Neither works.&lt;/p&gt;

&lt;p&gt;So I capture the image on the native side and decode it on the web side during idle time, before the switch. But a UI element can still be changing shape just after a long press ends. On iOS, I check its position and size at intervals roughly equal to one frame at 60fps, and capture it only after three consecutive checks find no change.&lt;/p&gt;

&lt;p&gt;When Projection starts, I temporarily show the same image on the native side to bridge the handoff. I place the image, captured with its background, and hide the real native UI in the same native-side operation. This prevents both the blank gap and the doubled translucent UI.&lt;/p&gt;

&lt;p&gt;Then I show the image on the web side and use &lt;code&gt;requestAnimationFrame&lt;/code&gt; to notify the native side to remove its temporary copy. The appearance stays the same. Only the rendering layer changes.&lt;/p&gt;

&lt;p&gt;Just watching the screen, you can't tell when the native UI becomes an image.&lt;/p&gt;

&lt;p&gt;It feels like magic!&lt;/p&gt;

&lt;p&gt;I've also tuned scrolling separately on iOS and Android so the native UI follows the content smoothly.&lt;/p&gt;

&lt;p&gt;&lt;a href="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/7hmiozfa42zm9t92yqtz.gif" class="article-body-image-wrapper"&gt;&lt;img src="//dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/7hmiozfa42zm9t92yqtz.gif" alt="On iOS and Android, native ads scroll between posts and disappear behind the fixed header." width="540"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Watch the top edge of each ad. On both platforms, it disappears behind the header.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  And you can use it today
&lt;/h2&gt;

&lt;p&gt;The first things I wanted to support were iOS navigation—toolbar buttons and a tab bar—and native ads that scroll within web content.&lt;/p&gt;

&lt;p&gt;This approach is already available in &lt;a href="https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui" rel="noopener noreferrer"&gt;@rdlabo/capacitor-native-ui&lt;/a&gt; and &lt;a href="https://github.com/capacitor-community/admob" rel="noopener noreferrer"&gt;@capacitor-community/admob&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But keeping it limited to the plugins I work on felt like a missed opportunity.&lt;/p&gt;

&lt;p&gt;Toolbars, mini-player controls, UI provided by native SDKs—there must be more places where this could help.&lt;/p&gt;

&lt;p&gt;So I extracted the shared pieces into &lt;a href="https://github.com/webview-native-ui/engine" rel="noopener noreferrer"&gt;WebView Native UI Engine&lt;/a&gt;, an open-source library under the MIT license.&lt;/p&gt;

&lt;p&gt;It's for Capacitor and Cordova plugin authors, with support for iOS and Android.&lt;/p&gt;

&lt;p&gt;Register a native view and its destination DOM container, then notify the Engine of the relevant state changes. It coordinates the display transitions. I designed it to fit into existing plugins while preserving their APIs and business logic.&lt;/p&gt;

&lt;p&gt;I'd love for you to try it in one of two ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you build plugins
&lt;/h3&gt;

&lt;p&gt;Whether you're building a new plugin with native UI or extending an existing one, try integrating the Engine.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/webview-native-ui/engine/blob/main/docs/getting-started/initialization.md" rel="noopener noreferrer"&gt;Getting Started guide&lt;/a&gt; includes a minimal setup and integration instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you use plugins
&lt;/h3&gt;

&lt;p&gt;Wouldn't it be interesting to have this in a plugin you already use?&lt;/p&gt;

&lt;p&gt;Suggest it to the plugin's author. Or, if you think you can implement it, send a pull request.&lt;/p&gt;

&lt;p&gt;There must be uses for this that I'd never think of on my own.&lt;/p&gt;

&lt;p&gt;I can't wait to see what you build with it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webview-native-ui/engine" rel="noopener noreferrer"&gt;WebView Native UI Engine on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>webdev</category>
      <category>mobile</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
