A Cordova port of Capacitor's SystemBars API for OutSystems 11 / MABS 12. The plugin exposes the same JavaScript surface as the Capacitor 8 bundled SystemBars plugin, reimplemented natively in Kotlin (Android) and Swift (iOS). There is no Capacitor dependency.
- Why this exists
- Features
- Requirements
- Installation
- Usage
- API reference
- Error handling
- Style semantics
- Declarative configuration (MABS 12 preferences)
- Platform notes
- Required theme CSS
- Differences from Capacitor's SystemBars
- Contributing
- License
MABS 12 / cordova-android 14 already cover the declarative side of system-bar configuration through native preferences (AndroidEdgeToEdge, EdgeToEdgeGlyphTheme, StatusBarBackgroundColor, NavigationBarBackgroundColor). What they do not offer is a clean runtime Promise-based API to change the status-bar style, hide bars temporarily, or animate transitions from JavaScript. This plugin fills that gap, exposing the same JavaScript surface as Capacitor's SystemBars plugin so the contract is familiar and well-documented. The plugin deliberately does not override the MABS preferences — set them in your Extensibility Configurations and they take effect.
- Strict API parity with Capacitor 8's
SystemBars: exactly the same four methods (setStyle,setAnimation,show,hide) with the same argument shapes and the same semantics. Combined with the bundledOSSystemBarsWrapper, a single Client Action can run unchanged on both O11 (Cordova) and ODC (Capacitor). - Promise-based JavaScript API.
- Runtime style toggling per bar (
StatusBar/NavigationBar) on Android. - Runtime visibility control with
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPEon Android so the user can still swipe to reveal hidden bars. - Configurable show/hide animation on iOS (
NONE/FADE). - Bakes
UIViewControllerBasedStatusBarAppearance = trueinto the iOSInfo.plistautomatically. - Does not fight MABS 12 platform preferences — declarative startup configuration is left entirely to the platform.
| Component | Minimum |
|---|---|
| OutSystems | 11, MABS 12+ |
cordova-android |
12+ (MABS 12 ships a compatible version) |
cordova-ios |
7.0+ (MABS 12 ships a compatible version) |
Android compileSdkVersion |
35 |
Android minSdkVersion |
24 |
| iOS deployment target | 13.0 |
| Swift | 5 |
Paste this into the Extensibility Configurations property of your OutSystems mobile app:
{
"plugin": {
"url": "https://github.com/promonteiro89/cordova-plugin-system-bars.git#1.0.4"
}
}Pin the tag (#1.0.2) so MABS does not silently pull breaking changes.
This plugin is a Cordova plugin and installs only on the O11 / MABS build target. On ODC, you don't need to install anything — @capacitor/core already exposes Capacitor.Plugins.SystemBars as a built-in. The two implementations expose the same Capacitor-spec API on different globals (cordova.plugins.SystemBars on O11, Capacitor.Plugins.SystemBars on ODC), so a single Client Action can run on both runtimes by resolving the access path once.
OutSystems Extensibility Configurations:
{
"plugin": {
"url": "https://github.com/promonteiro89/cordova-plugin-system-bars.git#1.0.4"
},
"buildConfigurations": {
"cordova": {
"source": {
"npm": "https://github.com/promonteiro89/cordova-plugin-system-bars.git#1.0.4"
}
}
},
"metadata": {
"mabs-min": "12.0.0",
"name": "SystemBars",
"version": "1.0.4"
}
}To make a Client Action that works on both runtimes without branching, either use the bundled wrapper (recommended), or pick the implementation manually:
const SystemBars =
(window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.SystemBars)
|| (window.cordova && window.cordova.plugins && window.cordova.plugins.SystemBars);
SystemBars.setStyle({ style: 'DARK' });Advanced: if you'd rather route all SystemBars calls through this Cordova plugin on ODC too (instead of
@capacitor/core's built-in), add a"capacitor": { "source": { "npm": "https://github.com/promonteiro89/cordova-plugin-system-bars.git#1.0.4" } }block alongsidecordovaand Capacitor will install this plugin via its Cordova-plugin compat layer. The iOS code already handles this case — it extends bothCDVViewControllerand (under#if canImport(Capacitor))CAPBridgeViewControllerso the status-bar overrides take effect on either root VC. This is unconventional (the OutSystems convention reservesbuildConfigurations.capacitorfor Capacitor-native packages) but functional.
For local testing in a vanilla Cordova project:
cordova plugin add https://github.com/promonteiro89/cordova-plugin-system-bars.git#1.0.4For OutSystems modules that target both O11 and ODC, the repo also ships a small dispatcher under packages/outsystems-wrapper/ that hides the runtime difference behind a single API surface. Copy the prebuilt packages/outsystems-wrapper/dist/outsystems.js into your module's scripts folder, RequireScript it, then call:
OSSystemBarsWrapper.Instance.setStyle({ style: 'DARK' });
OSSystemBarsWrapper.Instance.hide({ bar: 'StatusBar' });The wrapper resolves to Capacitor.Plugins.SystemBars on ODC builds and cordova.plugins.SystemBars on O11 builds. No translation is done — both APIs are already shape-compatible. See the wrapper README for full usage.
// Dark icons on a light background, both bars
cordova.plugins.SystemBars.setStyle({ style: 'LIGHT' });
// Light icons on a dark background, status bar only
cordova.plugins.SystemBars.setStyle({ style: 'DARK', bar: 'StatusBar' });
// Follow the device's system theme
cordova.plugins.SystemBars.setStyle({ style: 'DEFAULT' });
// Hide the status bar (Android: swipe to reveal)
cordova.plugins.SystemBars.hide({ bar: 'StatusBar' });
// Show everything again
cordova.plugins.SystemBars.show();
// Animate subsequent status-bar visibility changes on iOS
cordova.plugins.SystemBars.setAnimation({ animation: 'FADE' });All methods return a Promise<void> that rejects with a structured { code, message } error object on failure — see Error handling.
All methods are exposed on cordova.plugins.SystemBars.
Sets icon/text appearance on the system bars.
| Parameter | Type | Required | Description |
|---|---|---|---|
options.style |
'DARK' | 'LIGHT' | 'DEFAULT' |
yes | See Style semantics. |
options.bar |
'StatusBar' | 'NavigationBar' |
no | Apply to a single bar. Omit to apply to both (Android). On iOS bar is ignored — the style always targets the status bar, exactly as @capacitor/core's iOS implementation behaves. |
Sets the transition used when the status bar appearance changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
options.animation |
'NONE' | 'FADE' |
yes | iOS maps to UIStatusBarAnimation and uses it for the next show/hide/setStyle transition. Android validates the value to match Capacitor's input contract; the platform composes its own system-bar animation. |
Shows the system bars (or just one).
| Parameter | Type | Required | Description |
|---|---|---|---|
options.bar |
'StatusBar' | 'NavigationBar' |
no | Restrict to a single bar. Omit to show all. On iOS, NavigationBar targets the home indicator (re-enables it), matching Capacitor. |
options.animation |
'NONE' | 'FADE' |
no | Per-call animation (iOS only). Persists as the current animation for subsequent transitions — same effect as calling setAnimation first, matching Capacitor. |
Hides the system bars (or just one). On Android the controller is set to BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE, so the user can swipe to reveal the bars temporarily.
| Parameter | Type | Required | Description |
|---|---|---|---|
options.bar |
'StatusBar' | 'NavigationBar' |
no | Restrict to a single bar. Omit to hide all. On iOS, NavigationBar targets the home indicator (allows it to auto-hide), matching Capacitor. |
options.animation |
'NONE' | 'FADE' |
no | Per-call animation (iOS only). Persists as the current animation for subsequent transitions — same effect as calling setAnimation first, matching Capacitor. |
Every method returns Promise<void> and rejects with a structured error object — never a bare string — so consumers can branch on a stable code:
cordova.plugins.SystemBars.setAnimation({ animation: 'SLIDE' })
.catch(function (err) {
console.error(err.code); // "OS-PLUG-SYSBARS-0005"
console.error(err.message); // "The 'setAnimation' input parameters aren't valid: ..."
});Out-of-range style, bar, or animation values are rejected with OS-PLUG-SYSBARS-0005 rather than silently ignored.
| Code | Meaning |
|---|---|
OS-PLUG-SYSBARS-0005 |
Invalid input — an out-of-range style, bar, or animation value. |
OS-PLUG-SYSBARS-0010 |
Native window handle unavailable — Android's WindowInsetsController or iOS's root view controller. |
OS-PLUG-SYSBARS-0013 |
Operation failed — caught native-exception fallback (Android only). |
Watch out —
'DARK'refers to the background, not the icons. This is the most common bug when porting code.
| Value | Meaning | Android appearanceLight* |
iOS UIStatusBarStyle |
|---|---|---|---|
'DARK' |
Light icons/text on a dark background | false |
.lightContent |
'LIGHT' |
Dark icons/text on a light background | true |
.darkContent |
'DEFAULT' |
Follow the system theme | derived from uiMode (night → light icons) |
.default |
This mapping is taken verbatim from Capacitor's SystemBarsStyle.
MABS 12 / cordova-android 14 exposes four native Cordova preferences that cover the startup appearance of the system bars. They are applied by the platform itself — this plugin does not override them, so they remain available to OutSystems developers via Extensibility Configurations.
| Preference | Type | Description |
|---|---|---|
AndroidEdgeToEdge |
true / false |
Whether the app draws edge-to-edge on Android. Enforced as true on Android 15 (API 35+) regardless of this setting. |
EdgeToEdgeGlyphTheme |
dark / light |
Color of system-bar glyphs (icons and text) when edge-to-edge is enabled. dark = dark glyphs (use on light backgrounds); light = light glyphs (use on dark backgrounds). Falls back to luminance-based detection if not set or invalid. |
StatusBarBackgroundColor |
#RRGGBB |
Status bar background color. Only takes effect when edge-to-edge is disabled. |
NavigationBarBackgroundColor |
#RRGGBB |
Navigation bar background color. Only takes effect when edge-to-edge is disabled. |
⚠️ Glyph-color vs background-tone: MABS'sEdgeToEdgeGlyphThemeis the opposite convention of Capacitor'sstyle. MABSlight(light glyphs) matches oursetStyle({ style: 'DARK' })(dark background → light icons), and MABSdark(dark glyphs) matchessetStyle({ style: 'LIGHT' }).
These preferences belong in the consuming OutSystems mobile app's Extensibility Configurations — the same property where you added the plugin install URL — and not in this plugin's repository. Add only the ones you care about:
{
"preferences": {
"global": [
{ "name": "AndroidEdgeToEdge", "value": "true" },
{ "name": "EdgeToEdgeGlyphTheme", "value": "light" },
{ "name": "StatusBarBackgroundColor", "value": "#000000" },
{ "name": "NavigationBarBackgroundColor", "value": "#000000" }
]
}
}If you also need the plugin install entry in the same JSON, merge the two objects:
{
"preferences": {
"global": [
{ "name": "AndroidEdgeToEdge", "value": "true" },
{ "name": "EdgeToEdgeGlyphTheme", "value": "light" }
]
},
"plugin": {
"url": "https://github.com/promonteiro89/cordova-plugin-system-bars.git#1.0.4"
}
}All four preferences are optional and independent — drop any you don't need. The runtime JavaScript API (setStyle, show, hide, setAnimation) takes over after launch and overrides whatever appearance the preferences set.
- Implemented in Kotlin. The plugin opts the consuming app into the Kotlin Gradle plugin via the
GradlePluginKotlinEnabled/GradlePluginKotlinCodeStyleCordova preferences (injected into the Android platform'sconfig.xmlat install time) and ships the source underapp/src/main/kotlin/.... - Edge-to-edge and bar colors are not forced by this plugin. They are controlled by the MABS 12 preferences described in Declarative configuration; on Android 15 (API 35+) the platform itself enforces edge-to-edge regardless of
AndroidEdgeToEdge. - Style changes use
WindowInsetsControllerCompat.isAppearanceLight*Bars. - A runtime light/dark theme switch is honored: on a
uiModechange Android resets the bar appearance to the activity theme's default, discarding the activesetStyle. Becausecordova-androidkeepsuiModein the activity'sconfigChanges, the activity isn't recreated and the WebView isn't reloaded — so your JS never re-runs to restore it. The plugin re-applies the last style set for each bar on the change, so runtimeDARK/LIGHT/DEFAULTchoices survive the toggle — including different styles per bar (DEFAULTre-derives from the new theme). - Visibility uses
WindowInsetsControllerCompat.show()/hide()withWindowInsetsCompat.Type.statusBars(),navigationBars(), orsystemBars()as appropriate. setStyle({ bar: 'NavigationBar' })only has a visible effect in 3-button navigation mode, where the back/home/recents icons clearly switch between dark and light. In gesture navigation mode, only a thin gesture handle is drawn, and recent Android versions auto-adapt its luminance to the underlying content — the appearance flag is set but the change is barely perceptible. This is platform behavior, not a plugin bug.
- Implemented in Swift 5, targeting iOS 13+.
- iOS has no navigation bar, so
bar: 'NavigationBar'maps the way Capacitor maps it:setStyleignoresbar(the style always targets the status bar), whileshow/hidecontrol the home indicator —hide({ bar: 'NavigationBar' })lets it fade out after a moment of inactivity,showbrings it back. A bar-lessshow()/hide()affects both the status bar and the home indicator. - Status bar style is driven by overrides on both
CDVViewControllerand (under#if canImport(Capacitor))CAPBridgeViewController, since Capacitor's Cordova-compat layer routes throughCAPBridgeViewControllerrather thanCDVViewController. Same three overrides (preferredStatusBarStyle,prefersStatusBarHidden,preferredStatusBarUpdateAnimation) on each, plusprefersHomeIndicatorAutoHiddenonCDVViewControlleronly — Capacitor already installs its own equivalent onCAPBridgeViewController. - The plugin writes
UIViewControllerBasedStatusBarAppearance = trueinto the app'sInfo.plistautomatically — no extensibility tweak required.
When edge-to-edge is enabled (default on Android 15, or whenever AndroidEdgeToEdge=true is set), your app's chrome must paint behind the system bars and respect safe-area insets:
:root {
--sat: env(safe-area-inset-top, 0px);
--sab: env(safe-area-inset-bottom, 0px);
}
.app-header { padding-top: var(--sat); }
.app-footer { padding-bottom: var(--sab); }Make sure your viewport meta tag includes viewport-fit=cover, otherwise iOS reports safe-area insets as zero:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">- The plugin object lives at
cordova.plugins.SystemBarson O11 builds. ODC builds useCapacitor.Plugins.SystemBarsfrom@capacitor/core's built-in plugin (this Cordova plugin does not install on ODC by default). For Client Actions that run on both, use the bundled wrapper orconst SystemBars = window.Capacitor?.Plugins?.SystemBars ?? cordova.plugins.SystemBars;. - Capacitor's legacy Status Bar plugin (
setBackgroundColor,setOverlaysWebView) is not ported — those methods are intentionally not part of Capacitor'sSystemBarsAPI and are out of scope here too. - On Android,
setAnimationvalidates the value to match Capacitor's input contract but does not change the platform's system-bar animation (the OS composes its own). - Stricter input validation: invalid
style,bar, oranimationvalues reject withOS-PLUG-SYSBARS-0005. Capacitor silently coerces them instead (an unknownstylefalls back toDEFAULT, an unknownanimationtoFADE). Valid inputs behave identically on both.
See CONTRIBUTING.md for development workflow, the PR checklist, and how to file bug reports.
MIT © Paulo Ricardo Oliveira Monteiro