Skip to content

Cordova, Ionic & Capacitor

JOPS runs unchanged inside a mobile WebView, so the same app can ship to the App Store and Google Play through a hybrid shell. Nothing on this page changes how JOPS behaves in a regular browser: every mobile adjustment is either feature-detected (it only runs when the native shell is present) or a CSS value that resolves to 0 outside it.

Shell Status
Apache Cordova (iOS + Android) Supported and tested (cordova-ios 8, cordova-android 15)
Capacitor (iOS + Android) Supported and tested (Capacitor 8). Install @capacitor/app for the Android back button
Ionic Use Ionic’s framework-free UI components (@ionic/core) on top of Cordova or Capacitor
  1. Create the hybrid project and its platforms.

    Terminal window
    cordova create my-app com.example.myapp MyApp
    cd my-app
    cordova platform add ios android
  2. Install JOPS in the project root, the folder with package.json, not inside www/:

    Terminal window
    npm i jops-core

    Cordova and Capacitor copy the whole web folder into the app, so a node_modules/ folder there would be shipped too.

  3. Run init from the project root (running it from inside www/ or your webDir works too):

    Terminal window
    npx jops-core init

    init detects the project type and writes the app into the web folder the shell actually serves: www/ for Cordova, the webDir from capacitor.config.* for Capacitor.

    [jops-core] Detected cordova project → web folder: www/
  4. Build and run with the shell’s own tools.

    Terminal window
    cordova run ios
    cordova run android

    You can also open the native project in Xcode or Android Studio.

Web Cordova Capacitor
index.html, lib/, template current folder www/ webDir
.vscode/ and package.json scripts current folder project root project root
viewport-fit=cover — ✓ ✓
Content-Security-Policy — adds 'unsafe-inline' (inline import map) and blob: (Thread workers) to script-src — (Capacitor has none by default)
<script src="cordova.js"> — ✓ —
iOS custom scheme in config.xml — ✓ (app://localhost) — (Capacitor uses capacitor://localhost)
"build" script in package.json ✓ — (use cordova build) — (use npx cap sync)
"postinstall": "jops-core update" ✓ ✓ ✓
  • Safe to run again. Every step checks first, so running init again never duplicates anything.
  • postinstall. "jops-core update" copies the library from node_modules after every npm install. Projects created before 1.1.8 have "postinstall": "npx jops-core init", which shows the template question on every npm install (since 1.1.8 it uses 5, None, when nobody answers; before, npm install failed in CI). Change it to "postinstall": "jops-core update".
  • Cordova’s sample page. When you pick the Empty or Hello World template and www/index.html is still the page created by cordova create, init renames it to www/index.cordova.html and writes a JOPS index.html. No files are deleted; the sample’s css/, img/ and js/ folders can be removed by hand.
  • Updates and builds. npx jops-core update writes to the same web folder. npx jops-core build is skipped in hybrid projects, because the web folder is shipped as it is.
  • Override the detection with --cordova, --capacitor or --web. For example, npx jops-core init --web treats the current folder as a plain web app.

JOPS loads App.js, views and layouts with native import() and fetch(), which WebViews block on file:// pages. The app must be served from a real origin:

  • Cordova Android and Capacitor already serve from https://localhost (Capacitor iOS uses capacitor://localhost).

  • Cordova iOS: init adds a custom scheme to config.xml. It is applied on the next cordova prepare, build or run:

    config.xml
    <platform name="ios">
    <preference name="scheme" value="app" />
    <preference name="hostname" value="localhost" />
    </platform>

Values you already set are never changed, and any custom scheme works. init warns if the iOS scheme is file, http or https, or if AndroidInsecureFileModeEnabled is true, since both keep the app on file://.

Cordova projects come with a Content-Security-Policy. JOPS needs two additions to script-src: 'unsafe-inline' for the inline import map and blob: for Thread workers. This is the index.html that init produces for a new Cordova project:

www/index.html
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' data: blob: https://ssl.gstatic.com; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' data: blob:; media-src * blob:; img-src 'self' data: blob: content:;" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<script type="importmap">
{ "imports": { "jops-core": "/lib/jops-core.min.js" } }
</script>
</head>
<body>
<script src="cordova.js"></script>
<script type="module" src="/lib/jops-core.min.js" data-app="./src/App.js"></script>
</body>

Capacitor has no policy by default. If you add one, include the same two script-src sources. If your app calls a remote API, add its host to connect-src.

In Cordova and Capacitor, JOPS connects the Android back button (key and gesture) to the Router automatically:

  • If Router.singleton().canGoBack() is true, it calls Router.back(). The current view receives onBackPressed() exactly as with the browser’s Back.
  • On the first screen it closes the app (navigator.app.exitApp()), the standard Android behaviour.

This matters on Android 16 (targetSdk 36, predictive back): without a backbutton listener, Cordova lets the system handle back and the app closes on the first press, whatever its history. The handler also smooths over a cordova-android 15 issue where one key press fires backbutton twice.

The hook only activates on the deviceready event, which Cordova and Capacitor fire and a regular browser never does.

If your app handles backbutton itself, turn the built-in handler off with data-native-back="off" on the jops-core script:

www/index.html
<script type="module" src="/lib/jops-core.min.js" data-app="./src/App.js" data-native-back="off"></script>

Capacitor needs the official App plugin. No code is needed:

Terminal window
npm i @capacitor/app
npx cap sync

Capacitor’s Cordova compatibility layer fires deviceready and forwards JOPS’s backbutton listener to the plugin, so the behaviour above applies unchanged. Without @capacitor/app, Capacitor does not intercept back at all and the app closes on the first press; JOPS logs a console warning in that case.

To handle back yourself in Capacitor, opt out with data-native-back="off" and use the plugin’s global (no bundler needed):

import { Router } from "jops-core";
const { App } = window.Capacitor.Plugins;
App.addListener("backButton", () => {
const router = Router.singleton();
if (router?.canGoBack()) router.back();
else App.exitApp();
});

Notches, Dynamic Island and the home indicator

Section titled “Notches, Dynamic Island and the home indicator”

With viewport-fit=cover (added by init in Cordova and Capacitor projects), the page extends under the status bar and the home indicator. Pad fixed or sticky bars with the safe-area insets. They are 0 in a regular browser, so the layout does not change there:

www/src/css/styles.css
.header {
padding-top: calc(1rem + env(safe-area-inset-top, 0px));
}
.footer {
padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px));
}

The Hello World template already does this.

  • Android: open chrome://inspect in desktop Chrome while the emulator or a device runs the debug build.
  • iOS: in Safari, choose Develop → Simulator (or your device) → your app.

A cordova_plugins.js “not found” error in the console is expected when the project has no Cordova plugins installed. It is harmless.