You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Building capacitor-foldable I found UIKit updates UIHinge.status lazily, so it disagrees with the angle mid-fold: flat at 60 degrees, folded at 170, and stuck at closed for a moment after reopening. Simulator only so far, no hardware here. Table's in erkamyaman/capacitor-foldable#7.
Mild for the shell: a lazy status means one extra deviceLayoutChange when it corrects, and a stale hingeStatus in the payload until then. refreshHingeStatus() covers resume but not movement in between.
update.hinge?.angle is in the same handler if you want belt and braces. It's radians, and meaningless while status is .unknown. I let the angle override the status, 20 to 160 folded. I can send a PR if useful.
@erkamyaman Thank you for your interest in the project and your kind words! I really appreciate the detailed findings and your offer to send a PR.
Your issue made me reconsider whether device-state tracking belongs in the theme at all. Maintaining overlapping logic across both projects feels like unnecessary duplication, so I opened #230 to use @erkamyaman/capacitor-foldable for device state, update the demo, and remove that responsibility from the theme.
What do you think of this direction? I’d really appreciate your honest thoughts, including any concerns or trade-offs you see.
This made my week. I expected to point at a small lag and you read the whole thing and opened #230. Your project is the reason I knew what an iOS 27 shell should look like.
The direction is right, and I like that the theme takes edge and nativeEdge as inputs instead of importing the plugin. It still works for someone who never installs anything.
Reading the PR, your verticalBarEdge() fallback caught my eye: an app built against an SDK without the trait still gets the side bar, and my plugin returned null for it. My bug, not yours. Out as 8.3.2, same safe-area approach. On the Duo simulator the trait and the inference both report trailing where the old build reported null, so the class-based current fallback can go. Your range already allows it, the demo lockfile just pins 8.3.1.
inset is the one thing you're deleting that I don't cover, so nothing feeds --ios-theme-vertical-bars-native-inset after this. Say the word and I'll add it.
Small thing: with the string form, setVerticalControlAreaPlacement('trailing'), no nativeEdge is ever recorded, so nativeVerticalBars() trusts the native rail without a reported edge. Intentional?
Two notes for your users: ^8.3.1 won't resolve on Capacitor 7, that line is on the latest-7 tag. And foldStateChange can fire twice for one movement, so handlers want to be idempotent. Yours is. My numbers are still simulator only (erkamyaman/capacitor-foldable#4), so the folded thresholds may move on hardware, as a minor with a note.
I'm on X as @erkamyaman_ng if a DM is easier. Thank you again for this.
@erkamyaman Thank you! I'm really happy to be able to leave specialized device-state handling to your plugin.
I'd definitely welcome inset. Honestly, I don't trust Apple to keep that space at 80px forever. With possible future designs on iPad and other devices in mind, it would be great to receive the actual reserved width and updates when it changes.
For trailing, I don't want to restrict devices or use cases when an app developer deliberately chooses that placement. The goal is to bring the experience built on the web to native as much as possible, so attempting native projection with the string form is intentional.
That said, we currently fall back to the web when native rendering is unavailable, but we don't detect the case where native rendering succeeds without producing the requested vertical placement. Is your question about that behavior on, for example, an iPad without a native vertical rail? I'd appreciate hearing more about the case you have in mind.
Thank you for the 8.3.2 fix, too. I'll update the demo lockfile and review the current fallback. I've also noted the compatibility details, duplicate events, and the possibility of threshold changes after hardware testing. This integration assumes Capacitor 8.5 or later, so supporting earlier versions is outside its scope.
Thanks for sharing your X handle as well. If you're comfortable with it, I'd prefer to keep discussing this here in public, so other users and contributors can follow the reasoning behind the design as part of the open-source project.
inset is a good call, and you're right not to trust 80px. I'll add the measured
width to getBarPlacement() and fire barPlacementChange when it moves, so you
get the real number rather than my threshold.
On the string form, understood, and the case I meant is the one you named: native
rendering succeeds but no vertical rail exists, so the controls are hidden in the
web and never appear natively. An iPad, or an iPhone before 27.1, is exactly it. getBarPlacement() returning a null edge is a reliable "no rail here" signal if
you want to detect it.
Public is better, agreed, I'll keep it all in the issues. Thanks again for taking
this seriously.
@rdlaboinset is out in 8.3.3. Measured width of the bar on its edge in wholepoints, 84 on the Duo simulator, 0 where no bar is reported, and barPlacementChange fires when it changes.
@erkamyaman Thank you for the quick turnaround! I've updated #230 to use 8.3.3 and pass the measured inset on both the initial read and change notifications.
Your explanation also made me reconsider the trailing behavior. An app choosing a placement and a native rail actually existing there are separate things. Native vertical projection now stays disabled when nativeEdge has not been supplied or is null.
In that case, vertical-bars-only mode keeps the Web rail, while the full Native UI Shell returns to its ordinary horizontal layout. If a matching non-null nativeEdge is reported later, native vertical projection resumes.
This has helped clarify the responsibilities: the plugin handles specialized device-state reporting, and the theme handles rendering based on that information. Thank you for working through this with me, from the suggestions to the implementation!
@rdlabo Congratulations on landing it, and thank you for taking the nativeEdge question seriously. Keeping native projection off until a real edge is reported is the right call, and it is a better answer than the one I suggested.
The split reads clean now: the plugin says what the device is doing, the theme decides what to draw. This thread ended up being a decent public record of how the two projects agreed on that boundary, which should save the next person asking why we do not overlap.
I'll hold that API steady and tell you first if hardware moves anything after 23 October. Enjoy the release.
@erkamyaman Thank you! Your feedback and the plugin have been a huge help in getting this into shape.
I’ve now worked through the implementation and testing, and I’m planning to release v1.2.0 in the next few days. I’m still thinking about when to move Native UI Shell out of experimental status, though.
Everything is available to try in v1.2.0-3 already. If you have some time, I’d love for you to give it a try and hear what you think!
@rdlabo Tried 1.2.0-3 in a clean Ionic app rather than the demo: install the theme and the plugin, the stylesheet imports, enableVerticalControlArea(), then wire the four plugin APIs. It came up on the Duo simulator first try and passed {"verticalBarEdge":"trailing","inset":84} straight through to the theme. The setup docs matched what I actually had to do, which is rarer than it should be.
One idea, since the app is now the integration point: every app ends up writing the same mapping, and it is easy to leave out nativeEdge without noticing. A small adapter exported from the theme, something like setVerticalControlAreaPlacement(fromFoldable(placement)), would make the wiring one line while keeping the theme free of any dependency on the plugin.
On Native UI Shell leaving experimental, the vertical-bars path felt solid to me today. What is your own plan for it?
I also enjoyed the article, and our measurements line up exactly. Points 4 and 5, modals placing controls differently and backgrounds sitting behind the bars, were both new to me. One thought: a pointer to @erkamyaman/capacitor-foldable in there would help the readers who reach the device-state side of this and have nowhere to go next, and it would get the plugin in front of more people who need it. Your piece and your call, of course.
Thanks for trying it out so quickly! I’m relieved to hear it worked without any issues.
Regarding setVerticalControlAreaPlacement, I’m actually working on something a little ambitious: a minimal setup that lets apps benefit from vertical bars even without using the rest of this theme. I intend to maintain backward compatibility, but I’m still figuring out what the configuration API should look like, so I think the adapter will be a good next step once that settles down.
My plan is to move both Native UI Shell and vertical bars to preview in 1.2.0? I think stable will have to wait until Xcode 27.1 is officially released.
As for the article, I wrote this one with React Native and Flutter developers in mind as well. I’m planning a follow-up along the lines of “Supporting iPhone Duo in WebView Apps,” and I’d love to introduce @erkamyaman/capacitor-foldable there!
@rdlabo Pulled the branch before it landed and built it into the same test app as yesterday. Toolbar with two buttons coloured only through --background and --color, no fill attribute, and enableVerticalControlArea({ buttonDefaultFill: 'solid' }).
I could only check DOM state and bridge logs, not the rendered result, since I cannot drive the Duo simulator reliably, so treat this as mechanics rather than a visual confirmation. What I saw: ios-theme-vertical-bars on ion-app, one projected source with three members, rejectedControls: [], device half open in book posture, and the plugin reporting {"verticalBarEdge":"trailing","inset":84}.
One thing I could not tell from the outside. Once an app opts in globally, plain toolbar buttons also send a background, and since Ionic's clear fill sets --background: transparent those compute to rgba(0, 0, 0, 0) in my app. On the native side that still takes the tinted branch, so the button ends up prominent glass tinted with nothing and skips the border overlay in the else branch. You already guard exactly this value in ion-fab and the searchable tabs, so it may just be an oversight in the new path.
On where this is going: the part I find most interesting is the minimal setup you mentioned, apps getting vertical bars without adopting the whole theme. That is the version I would reach for, and it is also what makes the adapter worth adding after the config API settles rather than now, so your ordering seems right to me. Preview in 1.2.0 with stable after 27.1 is final sounds sensible from here, though you know the theme's users better than I do.
And yes please on the WebView follow-up. Ask me anything you need from the plugin side while you write it.
Thanks for checking this! I appreciate you looking at the DOM and bridge logs too.
Mapping solid buttons to .glassProminent in vertical bars is intentional. But I understand your point now: ordinary toolbar buttons without a custom background also end up using .glassProminent with a transparent tint. I’d like to check how that actually looks as well.
By the way, buttonDefaultFill: 'solid' is intended for apps that keep Ionic’s standard button styling. Did you test with only vertical-bars.css from this package? A small HTML/CSS example starting at ion-toolbar would really help!
No, I had not: my first run had the full theme CSS loaded, and both buttons had text labels, so neither was projection-eligible. Redone properly this time, only vertical-bars.css from the package, icon-only buttons, and the shell reports {"state":"native","projected":2,"updates":2}.
Setup: Ionic 9.0.5, mode: 'ios', theme 1.2.0-3 with #237, iPhone Duo simulator on 27.1.
// global.scss, nothing else from the package@import'@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
ion-button.styled { --background: #b8362a; --color: #fff8f2; }
Reading [part="native"] on each: the styled one gives rgb(184, 54, 42), which is your fix working, and the plain one gives rgba(0, 0, 0, 0). Neither has fill set, so Ionic marks both button-clear and the transparent value is stock Ionic, not your stylesheet.
I am not questioning the .glassProminent mapping, only what happens to the unstyled sibling. In the screenshot the plain magnifier comes out as a pale filled circle rather than the usual treatment, next to the red one.
You have the setup to judge whether that is acceptable. I can push this as a branch if it saves you rebuilding it.
@erkamyaman Thank you for adding those compatibility checks and for all your help! Your plugin is truly fantastic, and it has made this integration so much easier.
Thanks for waiting — v1.2.0 is now released! Native UI Shell and Vertical Bars are now in preview, and I’m really looking forward to getting Native UI Shell to stable. Excited to hear how it works in your test app!
@rdlabo Ran 1.2.0 in the test app: installs alongside the plugin, builds, runs, and both the tab bar and toolbar reach the rail. Your clear fix is confirmed, the plain button no longer sends a background.
One question the other way. Since defaultFill is now clear inside ion-buttons, and backgroundColor is only captured when the fill resolves to solid, I could not get a CSS background onto the rail for any icon-only toolbar action, even with an explicit fill="solid" where the DOM computes rgb(184, 54, 42). Both buttons render as plain glass.
Intended for icon-only actions, or should buttonDefaultFill: 'solid' still have a route inside ion-buttons?
@erkamyaman Thanks for testing 1.2.0 and confirming the clear-fill fix!
Yes, that appearance is intentional with the new default. In #243, I introduced buttonProjection: 'source' | 'system', with 'system' as the default. This lets SwiftUI choose the native appearance, making adoption easier for existing apps whose button styling depends on their surrounding backgrounds, including fill="clear" buttons.
To reflect Ionic’s fill and computed colors, opt into 'source':
You can also keep the global 'system' default and use data-projection="source" on an individual button or an ion-buttons group. For your styled button:
Both settings matter here: 'source' enables source styling, and fill="solid" enables the background. buttonDefaultFill: 'solid' does not override Ionic’s contextual clear default inside ion-buttons.
@rdlabo That explains it, thank you. buttonProjection: 'source' brings the colour straight through: the styled button is red on the rail and the plain one falls back to Ionic's clear styling. 'system' as the default makes sense to me now, an app that never asked for source styling should not get it.
Nothing further from my side. 1.2.0 works with the plugin, the tab bar and toolbar both project, and the fold state feeds it correctly.
@rdlabo Separately from the above: I split the plugin's agent skill into its own repo, since the existing iPhone Duo skills are all SwiftUI and UIKit and web-view apps had nowhere to land.
It covers the plugin side but says nothing about the theme yet, which is a gap. An agent helping someone with an Ionic app on a Duo should know ionic-theme-ios27 exists, and how vertical bars are set up. If you want, I will add a section for it, and you can tell me what it should say so it does not misrepresent your setup.
Thanks for confirming everything works! For the default appearance, I had apps with setups like white buttons on an ion-toolbar[color="dark"] in mind. Carrying those colors over to a different background can cause problems, so my first goal was to make existing apps work reasonably well without extra adjustments.
And thank you for offering to introduce the theme in your skill! I think a short overview and links to the official documentation would be enough:
ionic-theme-ios27 brings SwiftUI-like UI rearrangement to Ionic apps: it repositions existing ion-back-button, ion-buttons, ion-button, and ion-tab-bar controls, then projects them into native UI.
The plugin provides device state; the theme handles UI rearrangement and rendering.
Apps can adopt Vertical Bars while keeping their existing theme.
@rdlabo That framing works, and the dark toolbar example makes the default obvious. Section added, using your three points: what the theme rearranges and projects, the split between device state and rendering, and that Vertical Bars can be adopted without changing an app's theme. Setup points at your two doc pages rather than repeating them, so it cannot go stale.
@rdlabo One more that affects your users directly: 8.4.0 reads the fold on an app built with Xcode 27.0.
The hinge and reserved-region APIs arrived in the 27.1 SDK, so until now a build made with an older Xcode compiled them out and reported no fold at all, even running on an iPhone Duo. Those builds now reach the same APIs through the Objective-C runtime. Checked on the Duo simulator: an SDK 27.0 binary tracked the hinge from 0 to 180 degrees and moved to the inner display on the way. A build made with 27.1 keeps the typed path, so nothing changes there.
For the theme that means getFoldState and getBarPlacement now work for anyone still on the stable Xcode, which is most people until 27.1 ships. Worth knowing before you tell someone the vertical bars need a beta toolchain.
@erkamyaman Thanks again for all your help, from the device-state APIs and measured inset to testing the integration and introducing it in your skill. I also appreciate the update about 8.4.0 supporting older Xcode builds.
I’ve taken quite a few detours, but I finally feel happy with the API design for 2.0. I never imagined we’d get here, but it looks like iPhone Duo will be in people’s hands before Xcode 27.2 gets its final release. 😄
After a lot of thought, I’ve made @erkamyaman/capacitor-foldable part of the standard setup instructions for both the theme and Native UI. It’s a fantastic plugin, and being able to leave device-state reporting to it while giving apps control over their layout feels like the right division of responsibilities. Apps install it directly; we haven’t added a hidden dependency.
I’ve also overhauled the documentation and moved native control projection into its own package, @rdlabo/capacitor-native-ui. The updated entry points are the Native UI guide and Vertical Bars with an existing Ionic theme, which should also be useful for the links in your skill.
The latest prerelease is now 2.0.0-2, and I’m treating it as the release candidate for 2.0.0.
If you have some time, I’d love for you to take a look and let me know what you think. Thanks for helping shape this!
29. @erkamyaman Thanks again for all your help, from the device-state APIs and measured inset to testing the integration and introducing it in your skill. I also appreciate the update about 8.4.0 supporting older Xcode builds.
I’ve taken quite a few detours, but I finally feel happy with the API design for 2.0. I never imagined we’d get here, but it looks like iPhone Duo will be in people’s hands before Xcode 27.2 gets its final release. 😄
After a lot of thought, I’ve made @erkamyaman/capacitor-foldable part of the standard setup instructions for both the theme and Native UI. It’s a fantastic plugin, and being able to leave device-state reporting to it while giving apps control over their layout feels like the right division of responsibilities. Apps install it directly; we haven’t added a hidden dependency.
I’ve also overhauled the documentation and moved native control projection into its own package, @rdlabo/capacitor-native-ui. The updated entry points are the Native UI guide and Vertical Bars with an existing Ionic theme, which should also be useful for the links in your skill.
The latest prerelease is now 2.0.0-2, and I’m treating it as the release candidate for 2.0.0.
If you have some time, I’d love for you to take a look and let me know what you think. Thanks for helping shape this!
@rdlabo Spent the evening on 2.0.0-2. Migrated my Duo test app (Ionic 9 / Angular 22) off 1.2.0 and ran it on the iPhone Duo simulator, iOS 27.1, built with Xcode 27.1.
The migration guide was exact. Three changes and nothing else:
Build clean, cap sync ios lists the two plugins separately, nothing left over from the old Swift package. On device Foldable reports { verticalBarEdge: 'trailing', inset: 84 }, Native UI moves from state: 'web' to state: 'native', and the tab bar, the + and the search button all land in the rail. I also pushed synthetic fold states through applyFoldStateClasses to check the CSS: classes toggle correctly and --ios-theme-split-pane-width goes 320px to 233px on half-opened.
Three small things:
1. The snippets need Foldable 8.4.0.({ verticalBarEdge, inset }: BarPlacement) does not typecheck on 8.3.x, inset landed in 8.4.0. The install lines just say @erkamyaman/capacitor-foldable, so maybe pin @^8.4.0. The CSS degrades fine either way (max(80px, env(safe-area-inset-right))), so it is only a compile error.
2. Xcode 27.0 builds get no rail, silently.deviceEdge comes only from traitCollection.verticalBarEdge behind #if canImport(UIKit, _underlyingVersion: 9127.0.85). An app built with Xcode 27.0 running on a Duo on 27.1 gets null, so nativeVerticalBars() stays false and projection never leaves the web. Foldable still reports 'trailing' in that case (8.4.0 infers the edge from the safe-area inset when the trait is missing), so the two disagree and the developer has nothing to go on. You could read availability off Foldable's edge, or do the same inference. It is in RuntimeFoldSource.swift, MIT, take whatever is useful.
3. Two doc links point at themselves.docs/vertical-bars.md:13 and docs/ionic-vertical-bars.md:17 both link ../../capacitor-native-ui/docs/vertical-bars.md#use-with-capacitor-foldable, which from inside that package resolves back to the same file.
Two questions:
Cold start shows the ordinary horizontal layout for about a second before it crossfades to the rail. The crossfade itself looks good. I call enableVerticalControlArea() after Angular bootstraps, so some of that second is mine. Is there an earlier hook you would recommend, or is that just how it goes?
ios-theme-fold-expanded is set for half-opened and for flat with hinge bounds, but the 50vw split only keys off ios-theme-fold-half-opened. So a fully opened Duo keeps a 320px pane rather than splitting at the crease. Intended, or should expanded drive it too?
Thanks for the README and the setup docs, they read really well.
@erkamyaman Thank you for testing the migration so thoroughly! I put a lot of care into the documentation, so hearing that the guide matched what you actually needed to do makes me really happy.
And I’m sorry for asking you to make so many changes after we had already reached preview. I really appreciate you sticking with it.
On the three points:
Foldable version: Making the minimum version explicit is a good suggestion. One small detail: I checked the published source tags, and BarPlacement.inset is already present in 8.3.3, so that particular snippet doesn’t require 8.4.0. The minimum-version clarification isn’t included in docs: clarify rail startup and foldable split-pane widths #311 yet.
Older Xcode builds: You’re right about the SDK guard. Native rail support currently requires Xcode 27.1 or newer, as documented. Accepting Foldable’s edge alone would also require checking that our older-SDK SwiftUI toolbar path renders correctly, so I’m keeping that requirement for now. Your findings are helpful in explaining why the two plugins can report different availability.
Documentation links: Thank you for catching those. I’ve simplified both paths in docs: clarify rail startup and foldable split-pane widths #311. The link within vertical-bars.md intentionally points to a section on the same page, but the long relative path made that unnecessarily confusing.
You’re also right about startup timing. I’ve clarified the documentation in #311: start from the app root as soon as ion-app is mounted, without waiting for page data. Native projection is asynchronous, so that still doesn’t guarantee the first frame will already have the rail.
For the split pane, the distinction is intentional and follows the iPhone Duo Settings layout: the sidebar becomes narrower when fully open and reaches the crease when half-opened. expanded describes the inner-display state; it doesn’t mean the sidebar should always occupy half the screen.
@rdlabo#312 has it right, 8.3.3 is the floor. My mistake, I read it off the commit and assumed it landed in the next minor rather than checking the published types. Good catch on the verticalOnly guidance too, I had not noticed that one.
Thanks for the split pane explanation. Following the Settings layout makes sense and I had expanded backwards.
@erkamyaman Thanks for confirming! 2.0.0 is now available. 🎉
There is a lot of big news today, including PHP, so I am planning to share the release more widely in a few different places tomorrow.
Thank you for your continued support, from developing capacitor-foldable to testing the integration and discussing all these details with me. Your work and feedback are a huge help to both the theme and Native UI. I really appreciate the care you bring to this, and I look forward to continuing to improve the integration together!
Activity
rdlabo commentedon Sep 27, 2026
@erkamyaman Thank you for your interest in the project and your kind words! I really appreciate the detailed findings and your offer to send a PR.
Your issue made me reconsider whether device-state tracking belongs in the theme at all. Maintaining overlapping logic across both projects feels like unnecessary duplication, so I opened #230 to use
@erkamyaman/capacitor-foldablefor device state, update the demo, and remove that responsibility from the theme.What do you think of this direction? I’d really appreciate your honest thoughts, including any concerns or trade-offs you see.
erkamyaman commentedon Sep 27, 2026
@rdlabo
This made my week. I expected to point at a small lag and you read the whole thing and opened #230. Your project is the reason I knew what an iOS 27 shell should look like.
The direction is right, and I like that the theme takes
edgeandnativeEdgeas inputs instead of importing the plugin. It still works for someone who never installs anything.Reading the PR, your
verticalBarEdge()fallback caught my eye: an app built against an SDK without the trait still gets the side bar, and my plugin returned null for it. My bug, not yours. Out as 8.3.2, same safe-area approach. On the Duo simulator the trait and the inference both report trailing where the old build reported null, so the class-basedcurrentfallback can go. Your range already allows it, the demo lockfile just pins 8.3.1.insetis the one thing you're deleting that I don't cover, so nothing feeds--ios-theme-vertical-bars-native-insetafter this. Say the word and I'll add it.Small thing: with the string form,
setVerticalControlAreaPlacement('trailing'), nonativeEdgeis ever recorded, sonativeVerticalBars()trusts the native rail without a reported edge. Intentional?Two notes for your users:
^8.3.1won't resolve on Capacitor 7, that line is on thelatest-7tag. AndfoldStateChangecan fire twice for one movement, so handlers want to be idempotent. Yours is. My numbers are still simulator only (erkamyaman/capacitor-foldable#4), so the folded thresholds may move on hardware, as a minor with a note.I'm on X as @erkamyaman_ng if a DM is easier. Thank you again for this.
rdlabo commentedon Sep 27, 2026
@erkamyaman Thank you! I'm really happy to be able to leave specialized device-state handling to your plugin.
I'd definitely welcome
inset. Honestly, I don't trust Apple to keep that space at 80px forever. With possible future designs on iPad and other devices in mind, it would be great to receive the actual reserved width and updates when it changes.For
trailing, I don't want to restrict devices or use cases when an app developer deliberately chooses that placement. The goal is to bring the experience built on the web to native as much as possible, so attempting native projection with the string form is intentional.That said, we currently fall back to the web when native rendering is unavailable, but we don't detect the case where native rendering succeeds without producing the requested vertical placement. Is your question about that behavior on, for example, an iPad without a native vertical rail? I'd appreciate hearing more about the case you have in mind.
Thank you for the 8.3.2 fix, too. I'll update the demo lockfile and review the
currentfallback. I've also noted the compatibility details, duplicate events, and the possibility of threshold changes after hardware testing. This integration assumes Capacitor 8.5 or later, so supporting earlier versions is outside its scope.Thanks for sharing your X handle as well. If you're comfortable with it, I'd prefer to keep discussing this here in public, so other users and contributors can follow the reasoning behind the design as part of the open-source project.
erkamyaman commentedon Sep 27, 2026
@rdlabo Thank you, and for the follow too.
insetis a good call, and you're right not to trust 80px. I'll add the measuredwidth to
getBarPlacement()and firebarPlacementChangewhen it moves, so youget the real number rather than my threshold.
On the string form, understood, and the case I meant is the one you named: native
rendering succeeds but no vertical rail exists, so the controls are hidden in the
web and never appear natively. An iPad, or an iPhone before 27.1, is exactly it.
getBarPlacement()returning a null edge is a reliable "no rail here" signal ifyou want to detect it.
Public is better, agreed, I'll keep it all in the issues. Thanks again for taking
this seriously.
erkamyaman commentedon Sep 27, 2026
@rdlabo
insetis out in 8.3.3. Measured width of the bar on its edge in wholepoints, 84 on the Duo simulator, 0 where no bar is reported, andbarPlacementChangefires when it changes.rdlabo commentedon Sep 28, 2026
@erkamyaman Thank you for the quick turnaround! I've updated #230 to use 8.3.3 and pass the measured
inseton both the initial read and change notifications.Your explanation also made me reconsider the
trailingbehavior. An app choosing a placement and a native rail actually existing there are separate things. Native vertical projection now stays disabled whennativeEdgehas not been supplied or isnull.In that case, vertical-bars-only mode keeps the Web rail, while the full Native UI Shell returns to its ordinary horizontal layout. If a matching non-null
nativeEdgeis reported later, native vertical projection resumes.This has helped clarify the responsibilities: the plugin handles specialized device-state reporting, and the theme handles rendering based on that information. Thank you for working through this with me, from the suggestions to the implementation!
erkamyaman commentedon Sep 28, 2026
@rdlabo Congratulations on landing it, and thank you for taking the
nativeEdgequestion seriously. Keeping native projection off until a real edge is reported is the right call, and it is a better answer than the one I suggested.The split reads clean now: the plugin says what the device is doing, the theme decides what to draw. This thread ended up being a decent public record of how the two projects agreed on that boundary, which should save the next person asking why we do not overlap.
I'll hold that API steady and tell you first if hardware moves anything after 23 October. Enjoy the release.
rdlabo commentedon Sep 28, 2026
@erkamyaman Thank you! Your feedback and the plugin have been a huge help in getting this into shape.
I’ve now worked through the implementation and testing, and I’m planning to release v1.2.0 in the next few days. I’m still thinking about when to move Native UI Shell out of experimental status, though.
Everything is available to try in v1.2.0-3 already. If you have some time, I’d love for you to give it a try and hear what you think!
I also wrote up what I learned from all that simulator testing: An iPhone Duo Review for Cross-Platform App Developers. Hopefully some of the findings will be useful for your work too!
erkamyaman commentedon Sep 28, 2026
@rdlabo Tried 1.2.0-3 in a clean Ionic app rather than the demo: install the theme and the plugin, the stylesheet imports,
enableVerticalControlArea(), then wire the four plugin APIs. It came up on the Duo simulator first try and passed{"verticalBarEdge":"trailing","inset":84}straight through to the theme. The setup docs matched what I actually had to do, which is rarer than it should be.One idea, since the app is now the integration point: every app ends up writing the same mapping, and it is easy to leave out
nativeEdgewithout noticing. A small adapter exported from the theme, something likesetVerticalControlAreaPlacement(fromFoldable(placement)), would make the wiring one line while keeping the theme free of any dependency on the plugin.On Native UI Shell leaving experimental, the vertical-bars path felt solid to me today. What is your own plan for it?
I also enjoyed the article, and our measurements line up exactly. Points 4 and 5, modals placing controls differently and backgrounds sitting behind the bars, were both new to me. One thought: a pointer to
@erkamyaman/capacitor-foldablein there would help the readers who reach the device-state side of this and have nowhere to go next, and it would get the plugin in front of more people who need it. Your piece and your call, of course.rdlabo commentedon Sep 28, 2026
Thanks for trying it out so quickly! I’m relieved to hear it worked without any issues.
Regarding
setVerticalControlAreaPlacement, I’m actually working on something a little ambitious: a minimal setup that lets apps benefit from vertical bars even without using the rest of this theme. I intend to maintain backward compatibility, but I’m still figuring out what the configuration API should look like, so I think the adapter will be a good next step once that settles down.If you have a moment, take a look at PR #237 and this video : from other custom theme https://github.com/capawesome-team/capacitor-angular-starter-guide-demo/ . I’m starting to wonder where exactly I’m going with all this myself. 😄
My plan is to move both Native UI Shell and vertical bars to preview in 1.2.0? I think stable will have to wait until Xcode 27.1 is officially released.
As for the article, I wrote this one with React Native and Flutter developers in mind as well. I’m planning a follow-up along the lines of “Supporting iPhone Duo in WebView Apps,” and I’d love to introduce
@erkamyaman/capacitor-foldablethere!erkamyaman commentedon Sep 28, 2026
@rdlabo Pulled the branch before it landed and built it into the same test app as yesterday. Toolbar with two buttons coloured only through
--backgroundand--color, nofillattribute, andenableVerticalControlArea({ buttonDefaultFill: 'solid' }).I could only check DOM state and bridge logs, not the rendered result, since I cannot drive the Duo simulator reliably, so treat this as mechanics rather than a visual confirmation. What I saw:
ios-theme-vertical-barsonion-app, one projected source with three members,rejectedControls: [], device half open in book posture, and the plugin reporting{"verticalBarEdge":"trailing","inset":84}.One thing I could not tell from the outside. Once an app opts in globally, plain toolbar buttons also send a background, and since Ionic's clear fill sets
--background: transparentthose compute torgba(0, 0, 0, 0)in my app. On the native side that still takes the tinted branch, so the button ends up prominent glass tinted with nothing and skips the border overlay in the else branch. You already guard exactly this value inion-faband the searchable tabs, so it may just be an oversight in the new path.On where this is going: the part I find most interesting is the minimal setup you mentioned, apps getting vertical bars without adopting the whole theme. That is the version I would reach for, and it is also what makes the adapter worth adding after the config API settles rather than now, so your ordering seems right to me. Preview in 1.2.0 with stable after 27.1 is final sounds sensible from here, though you know the theme's users better than I do.
And yes please on the WebView follow-up. Ask me anything you need from the plugin side while you write it.
rdlabo commentedon Sep 28, 2026
Thanks for checking this! I appreciate you looking at the DOM and bridge logs too.
Mapping solid buttons to
.glassProminentin vertical bars is intentional. But I understand your point now: ordinary toolbar buttons without a custom background also end up using.glassProminentwith a transparent tint. I’d like to check how that actually looks as well.By the way,
buttonDefaultFill: 'solid'is intended for apps that keep Ionic’s standard button styling. Did you test with onlyvertical-bars.cssfrom this package? A small HTML/CSS example starting ation-toolbarwould really help!erkamyaman commentedon Sep 28, 2026
@rdlabo
No, I had not: my first run had the full theme CSS loaded, and both buttons had text labels, so neither was projection-eligible. Redone properly this time, only
vertical-bars.cssfrom the package, icon-only buttons, and the shell reports{"state":"native","projected":2,"updates":2}.Setup: Ionic 9.0.5,
mode: 'ios', theme 1.2.0-3 with #237, iPhone Duo simulator on 27.1.Reading
[part="native"]on each: the styled one givesrgb(184, 54, 42), which is your fix working, and the plain one givesrgba(0, 0, 0, 0). Neither hasfillset, so Ionic marks bothbutton-clearand the transparent value is stock Ionic, not your stylesheet.I am not questioning the
.glassProminentmapping, only what happens to the unstyled sibling. In the screenshot the plain magnifier comes out as a pale filled circle rather than the usual treatment, next to the red one.You have the setup to judge whether that is acceptable. I can push this as a branch if it saves you rebuilding it.
4 remaining items
rdlabo commentedon Sep 30, 2026
@erkamyaman Thank you for adding those compatibility checks and for all your help! Your plugin is truly fantastic, and it has made this integration so much easier.
Thanks for waiting — v1.2.0 is now released! Native UI Shell and Vertical Bars are now in preview, and I’m really looking forward to getting Native UI Shell to stable. Excited to hear how it works in your test app!
erkamyaman commentedon Sep 30, 2026
@rdlabo Ran 1.2.0 in the test app: installs alongside the plugin, builds, runs, and both the tab bar and toolbar reach the rail. Your
clearfix is confirmed, the plain button no longer sends a background.One question the other way. Since
defaultFillis nowclearinsideion-buttons, andbackgroundColoris only captured when the fill resolves tosolid, I could not get a CSS background onto the rail for any icon-only toolbar action, even with an explicitfill="solid"where the DOM computesrgb(184, 54, 42). Both buttons render as plain glass.Intended for icon-only actions, or should
buttonDefaultFill: 'solid'still have a route insideion-buttons?rdlabo commentedon Sep 30, 2026
@erkamyaman Thanks for testing 1.2.0 and confirming the clear-fill fix!
Yes, that appearance is intentional with the new default. In #243, I introduced
buttonProjection: 'source' | 'system', with'system'as the default. This lets SwiftUI choose the native appearance, making adoption easier for existing apps whose button styling depends on their surrounding backgrounds, includingfill="clear"buttons.To reflect Ionic’s fill and computed colors, opt into
'source':You can also keep the global
'system'default and usedata-projection="source"on an individual button or anion-buttonsgroup. For your styled button:Both settings matter here:
'source'enables source styling, andfill="solid"enables the background.buttonDefaultFill: 'solid'does not override Ionic’s contextualcleardefault insideion-buttons.The full details and examples are in the Vertical Bars documentation.
erkamyaman commentedon Sep 30, 2026
@rdlabo That explains it, thank you.
buttonProjection: 'source'brings the colour straight through: the styled button is red on the rail and the plain one falls back to Ionic's clear styling.'system'as the default makes sense to me now, an app that never asked for source styling should not get it.Nothing further from my side. 1.2.0 works with the plugin, the tab bar and toolbar both project, and the fold state feeds it correctly.
erkamyaman commentedon Sep 30, 2026
@rdlabo Separately from the above: I split the plugin's agent skill into its own repo, since the existing iPhone Duo skills are all SwiftUI and UIKit and web-view apps had nowhere to land.
https://github.com/erkamyaman/iphone-duo-capacitor-skills
It covers the plugin side but says nothing about the theme yet, which is a gap. An agent helping someone with an Ionic app on a Duo should know ionic-theme-ios27 exists, and how vertical bars are set up. If you want, I will add a section for it, and you can tell me what it should say so it does not misrepresent your setup.
rdlabo commentedon Sep 30, 2026
Thanks for confirming everything works! For the default appearance, I had apps with setups like white buttons on an
ion-toolbar[color="dark"]in mind. Carrying those colors over to a different background can cause problems, so my first goal was to make existing apps work reasonably well without extra adjustments.And thank you for offering to introduce the theme in your skill! I think a short overview and links to the official documentation would be enough:
ionic-theme-ios27brings SwiftUI-like UI rearrangement to Ionic apps: it repositions existingion-back-button,ion-buttons,ion-button, andion-tab-barcontrols, then projects them into native UI.For implementation details, the skill could direct agents to iPhone Duo with your existing theme and the Vertical Bars reference, rather than duplicating the API details and setup examples.
I think that would give agents enough context to recognize when the theme is useful and read the docs when setting it up. What do you think?
erkamyaman commentedon Sep 30, 2026
@rdlabo That framing works, and the dark toolbar example makes the default obvious. Section added, using your three points: what the theme rearranges and projects, the split between device state and rendering, and that Vertical Bars can be adopted without changing an app's theme. Setup points at your two doc pages rather than repeating them, so it cannot go stale.
https://github.com/erkamyaman/iphone-duo-capacitor-skills/blob/main/skills/capacitor-foldable/references/ionic.md
Tell me if any of the wording misrepresents it and I will change it.
erkamyaman commentedon Oct 1, 2026
@rdlabo One more that affects your users directly: 8.4.0 reads the fold on an app built with Xcode 27.0.
The hinge and reserved-region APIs arrived in the 27.1 SDK, so until now a build made with an older Xcode compiled them out and reported no fold at all, even running on an iPhone Duo. Those builds now reach the same APIs through the Objective-C runtime. Checked on the Duo simulator: an SDK 27.0 binary tracked the hinge from 0 to 180 degrees and moved to the inner display on the way. A build made with 27.1 keeps the typed path, so nothing changes there.
For the theme that means
getFoldStateandgetBarPlacementnow work for anyone still on the stable Xcode, which is most people until 27.1 ships. Worth knowing before you tell someone the vertical bars need a beta toolchain.rdlabo commentedon Oct 20, 2026
@erkamyaman Thanks again for all your help, from the device-state APIs and measured inset to testing the integration and introducing it in your skill. I also appreciate the update about 8.4.0 supporting older Xcode builds.
I’ve taken quite a few detours, but I finally feel happy with the API design for 2.0. I never imagined we’d get here, but it looks like iPhone Duo will be in people’s hands before Xcode 27.2 gets its final release. 😄
After a lot of thought, I’ve made
@erkamyaman/capacitor-foldablepart of the standard setup instructions for both the theme and Native UI. It’s a fantastic plugin, and being able to leave device-state reporting to it while giving apps control over their layout feels like the right division of responsibilities. Apps install it directly; we haven’t added a hidden dependency.I’ve also overhauled the documentation and moved native control projection into its own package,
@rdlabo/capacitor-native-ui. The updated entry points are the Native UI guide and Vertical Bars with an existing Ionic theme, which should also be useful for the links in your skill.The latest prerelease is now 2.0.0-2, and I’m treating it as the release candidate for 2.0.0.
If you have some time, I’d love for you to take a look and let me know what you think. Thanks for helping shape this!
erkamyaman commentedon Oct 20, 2026
@rdlabo thank you! I will have a look tonight!
erkamyaman commentedon Oct 20, 2026
@rdlabo Spent the evening on 2.0.0-2. Migrated my Duo test app (Ionic 9 / Angular 22) off 1.2.0 and ran it on the iPhone Duo simulator, iOS 27.1, built with Xcode 27.1.
The migration guide was exact. Three changes and nothing else:
@rdlabo/ionic-theme-ios27/vertical-bars→@rdlabo/capacitor-native-ui/vertical-barsbuttonProjection→ionic.verticalBars.buttonProjectionsetVerticalControlAreaPlacement({ edge, nativeEdge, inset })→setVerticalBarsLayout({ edge, inset })Build clean,
cap sync ioslists the two plugins separately, nothing left over from the old Swift package. On device Foldable reports{ verticalBarEdge: 'trailing', inset: 84 }, Native UI moves fromstate: 'web'tostate: 'native', and the tab bar, the+and the search button all land in the rail. I also pushed synthetic fold states throughapplyFoldStateClassesto check the CSS: classes toggle correctly and--ios-theme-split-pane-widthgoes 320px to 233px onhalf-opened.Three small things:
1. The snippets need Foldable 8.4.0.
({ verticalBarEdge, inset }: BarPlacement)does not typecheck on 8.3.x,insetlanded in 8.4.0. The install lines just say@erkamyaman/capacitor-foldable, so maybe pin@^8.4.0. The CSS degrades fine either way (max(80px, env(safe-area-inset-right))), so it is only a compile error.2. Xcode 27.0 builds get no rail, silently.
deviceEdgecomes only fromtraitCollection.verticalBarEdgebehind#if canImport(UIKit, _underlyingVersion: 9127.0.85). An app built with Xcode 27.0 running on a Duo on 27.1 getsnull, sonativeVerticalBars()stays false and projection never leaves the web. Foldable still reports'trailing'in that case (8.4.0 infers the edge from the safe-area inset when the trait is missing), so the two disagree and the developer has nothing to go on. You could read availability off Foldable's edge, or do the same inference. It is inRuntimeFoldSource.swift, MIT, take whatever is useful.3. Two doc links point at themselves.
docs/vertical-bars.md:13anddocs/ionic-vertical-bars.md:17both link../../capacitor-native-ui/docs/vertical-bars.md#use-with-capacitor-foldable, which from inside that package resolves back to the same file.Two questions:
Cold start shows the ordinary horizontal layout for about a second before it crossfades to the rail. The crossfade itself looks good. I call
enableVerticalControlArea()after Angular bootstraps, so some of that second is mine. Is there an earlier hook you would recommend, or is that just how it goes?ios-theme-fold-expandedis set forhalf-openedand forflatwith hinge bounds, but the 50vw split only keys offios-theme-fold-half-opened. So a fully opened Duo keeps a 320px pane rather than splitting at the crease. Intended, or shouldexpandeddrive it too?Thanks for the README and the setup docs, they read really well.
rdlabo commentedon Oct 21, 2026
@erkamyaman Thank you for testing the migration so thoroughly! I put a lot of care into the documentation, so hearing that the guide matched what you actually needed to do makes me really happy.
And I’m sorry for asking you to make so many changes after we had already reached preview. I really appreciate you sticking with it.
On the three points:
BarPlacement.insetis already present in 8.3.3, so that particular snippet doesn’t require 8.4.0. The minimum-version clarification isn’t included in docs: clarify rail startup and foldable split-pane widths #311 yet.vertical-bars.mdintentionally points to a section on the same page, but the long relative path made that unnecessarily confusing.You’re also right about startup timing. I’ve clarified the documentation in #311: start from the app root as soon as
ion-appis mounted, without waiting for page data. Native projection is asynchronous, so that still doesn’t guarantee the first frame will already have the rail.For the split pane, the distinction is intentional and follows the iPhone Duo Settings layout: the sidebar becomes narrower when fully open and reaches the crease when half-opened.
expandeddescribes the inner-display state; it doesn’t mean the sidebar should always occupy half the screen.Thanks again for the careful feedback!
erkamyaman commentedon Oct 21, 2026
@rdlabo #312 has it right, 8.3.3 is the floor. My mistake, I read it off the commit and assumed it landed in the next minor rather than checking the published types. Good catch on the
verticalOnlyguidance too, I had not noticed that one.Thanks for the split pane explanation. Following the Settings layout makes sense and I had
expandedbackwards.rdlabo commentedon Oct 22, 2026
@erkamyaman Thanks for confirming! 2.0.0 is now available. 🎉
There is a lot of big news today, including PHP, so I am planning to share the release more widely in a few different places tomorrow.
Thank you for your continued support, from developing capacitor-foldable to testing the integration and discussing all these details with me. Your work and feedback are a huge help to both the theme and Native UI. I really appreciate the care you bring to this, and I look forward to continuing to improve the integration together!
erkamyaman commentedon Oct 22, 2026
@rdlabo always feel free to reach out here!