NativeScript 9: Native Apps with JavaScript
HOW TO GUIDES Feb. 7, 2026, 5:30 p.m.

NativeScript 9: Native Apps with JavaScript

NativeScript 9 has turned the dream of writing truly native mobile apps with plain JavaScript into a day‑to‑day reality. The framework bridges the gap between web developers and the native world, letting you tap into iOS and Android APIs without learning Swift or Kotlin. In this guide we’ll walk through the core concepts, set up a fresh project, and build two real‑world features that showcase the power of NativeScript 9.

Why NativeScript Still Matters in 2026

Even with the rise of Flutter and React Native, NativeScript offers a unique blend of performance and flexibility. Because it runs JavaScript directly on the device, you get zero‑overhead bridges and full access to the underlying platform. This means UI components are rendered by the native UI toolkit, not a canvas or WebView, delivering a buttery‑smooth experience.

Another advantage is the “write once, use everywhere” philosophy that truly respects platform conventions. Your JavaScript code can call any Objective‑C/Swift or Java/Kotlin API, and the UI is described in XML that maps 1:1 to native views. This makes it easier to adopt incremental migration strategies for existing native apps.

Getting Started: Installing the CLI

The first step is installing the NativeScript CLI, which now ships with Node 20+ support out of the box. Open a terminal and run:

npm install -g @nativescript/cli

After the installation, verify the version:

ns --version

If you see a version number starting with 9, you’re ready to roll. The CLI scaffolds projects, runs emulators, and even publishes to the App Store directly from the command line.

Creating Your First Project

Run the following command to bootstrap a brand‑new NativeScript app using the JavaScript template:

ns create MyNativeApp --js

The CLI creates a folder structure that mirrors a typical mobile project: app/ holds UI markup, CSS, and JavaScript; platforms/ contains the generated iOS and Android projects; and node_modules/ houses all dependencies.

Navigate into the folder and launch the app on an Android emulator:

cd MyNativeApp
ns run android --emulator

If everything is set up correctly, you’ll see the default “Hello, World!” screen rendered by a native TextView on Android and a UILabel on iOS.

Understanding the UI Layer: XML + CSS

NativeScript separates layout (XML) from styling (CSS), much like the web. Each XML tag maps directly to a native widget. For example, <Button> becomes UIButton on iOS and android.widget.Button on Android.

Here’s a simple login screen markup:

<Page class="page">
    <StackLayout class="form">
        <TextField hint="Email" keyboardType="email"></TextField>
        <TextField hint="Password" secure="true"></TextField>
        <Button text="Sign In" tap="{{ onSignIn }}" class="btn-primary"></Button>
    </StackLayout>
</Page>

The accompanying CSS can be placed in app.css:

.page { background-color: #f8f9fa; }
.form { margin: 30; }
.btn-primary {
    background-color: #007bff;
    color: white;
    padding: 12;
    border-radius: 4;
}

Notice the use of “dp”‑like units (just plain numbers) – NativeScript automatically converts them to device‑specific pixels.

Binding Data with JavaScript

NativeScript’s data binding works with plain JavaScript objects. In the app.js file you can expose a view‑model like this:

const { Observable } = require("@nativescript/core");

function createViewModel() {
    const viewModel = new Observable();
    viewModel.email = "";
    viewModel.password = "";
    viewModel.onSignIn = () => {
        console.log(`Signing in ${viewModel.email}`);
        // Add real authentication logic here
    };
    return viewModel;
}

exports.onNavigatingTo = (args) => {
    const page = args.object;
    page.bindingContext = createViewModel();
};

The XML tap="{{ onSignIn }}" attribute now points to the onSignIn function defined in the view‑model. This pattern scales nicely for larger apps.

Accessing Native APIs Directly

One of NativeScript’s strongest features is the ability to call native APIs without any wrappers. The tns-core-modules (now @nativescript/core) package provides a thin abstraction layer, but you can also drop down to the platform objects.

For iOS, every native class is exposed under the global objc namespace. For Android, you use the android global. Here’s a quick example that reads the device’s battery level on both platforms:

function getBatteryLevel() {
    if (global.isIOS) {
        const device = UIDevice.currentDevice;
        const battery = device.batteryLevel; // returns 0.0–1.0
        return Math.round(battery * 100) + "%";
    } else {
        const context = utils.ad.getApplicationContext();
        const intentFilter = new android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED);
        const batteryIntent = context.registerReceiver(null, intentFilter);
        const level = batteryIntent.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1);
        const scale = batteryIntent.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1);
        return Math.round((level / scale) * 100) + "%";
    }
}

This function can be bound to a UI label, giving you live battery feedback without any third‑party plugins.

Pro Tip: When accessing native APIs, always guard your code with if (global.isIOS) or if (global.isAndroid). The compiler can’t infer types across platforms, and unchecked calls will crash on the wrong OS.

Example 1: Capturing a Photo with the Device Camera

Let’s build a feature that opens the native camera, captures a photo, and displays it in the app. We’ll use the camera module from @nativescript/camera, which is a thin wrapper around the platform APIs.

First, install the plugin:

ns plugin add @nativescript/camera

Next, create a simple UI with a button and an Image placeholder:

<Page navigatingTo="{{ onNavigatingTo }}" class="page">
    <StackLayout>
        <Button text="Take Photo" tap="{{ onTakePhoto }}" class="btn-primary"/>
        <Image src="{{ photoSource }}" stretch="aspectFit" height="300"/>
    </StackLayout>
</Page>

Now, implement the JavaScript logic:

const { Observable } = require("@nativescript/core");
const { requestPermissions, isAvailable, takePicture } = require("@nativescript/camera");

function createViewModel() {
    const vm = new Observable();
    vm.photoSource = "";
    vm.onTakePhoto = async () => {
        try {
            const granted = await requestPermissions();
            if (!granted) {
                console.log("Camera permission denied");
                return;
            }
            if (!isAvailable()) {
                console.log("Camera not available on this device");
                return;
            }
            const imageAsset = await takePicture({ width: 300, height: 300, keepAspectRatio: true });
            vm.set("photoSource", imageAsset);
        } catch (err) {
            console.error("Error taking picture:", err);
        }
    };
    return vm;
}

exports.onNavigatingTo = (args) => {
    const page = args.object;
    page.bindingContext = createViewModel();
};

The takePicture call returns an ImageAsset that the Image component can render directly. Run the app on a physical device to see the camera UI appear, capture a photo, and watch it populate the placeholder instantly.

Example 2: Real‑Time Geolocation + Map Integration

Many mobile apps need to show the user’s current location on a map. NativeScript 9 ships with the @nativescript/geolocation and @nativescript/google-maps-sdk (Android) / @nativescript/mapbox (iOS) plugins, but you can also call the native map APIs directly for tighter control.

First, add the required plugins:

ns plugin add @nativescript/geolocation
ns plugin add @nativescript/google-maps-sdk

Next, define a page that contains a MapView component:

<Page navigatingTo="{{ onNavigatingTo }}" class="page">
    <StackLayout>
        <MapView latitude="{{ latitude }}" longitude="{{ longitude }}" zoom="{{ zoom }}" height="400"/>
        <Button text="Refresh Location" tap="{{ onRefresh }}" class="btn-primary"/>
    </StackLayout>
</Page>

Now, wire up the JavaScript to fetch the device’s GPS coordinates and update the map:

const { Observable } = require("@nativescript/core");
const { isEnabled, enableLocationRequest, getCurrentLocation } = require("@nativescript/geolocation");

function createViewModel() {
    const vm = new Observable();
    vm.latitude = 0;
    vm.longitude = 0;
    vm.zoom = 15;

    vm.updateLocation = async () => {
        const enabled = await isEnabled();
        if (!enabled) {
            await enableLocationRequest();
        }
        const location = await getCurrentLocation({ desiredAccuracy: 3, updateDistance: 10, maximumAge: 5000 });
        vm.set("latitude", location.latitude);
        vm.set("longitude", location.longitude);
    };

    vm.onRefresh = () => vm.updateLocation();

    return vm;
}

exports.onNavigatingTo = (args) => {
    const page = args.object;
    page.bindingContext = createViewModel();
    page.bindingContext.updateLocation();
};

When you launch the app, the map centers on the current GPS fix, and tapping “Refresh Location” moves the pin as you walk around. This pattern works for delivery tracking, fitness apps, or any location‑aware service.

Pro Tip: Always request location permissions at runtime and handle the case where the user denies them. On Android 12+, you also need to specify android:foregroundServiceType="location" in the manifest if you plan to run background tracking.

Performance Optimizations for Production

NativeScript’s runtime is already lightweight, but a few best practices can shave milliseconds off startup and frame rendering times. First, enable --bundle when building for release; this bundles your JavaScript into a single file and applies tree‑shaking.

Second, avoid heavy computations on the UI thread. Use setTimeout or the worker_threads API to offload work to a background isolate. Third, leverage layoutCache on complex list views to recycle native cells efficiently.

Finally, profile your app with the native tools (Xcode Instruments, Android Profiler). Look for “jank” spikes in the UI thread and address them by simplifying bindings or reducing layout depth.

Debugging and Hot Reload

NativeScript 9 ships with an improved hot‑reload experience that works for both JavaScript and XML changes. Run the app with:

ns run ios --watch

Any file save triggers an instant UI refresh without rebuilding the native project. For deeper debugging, attach Chrome DevTools (Android) or Safari Web Inspector (iOS) to inspect the JavaScript context.

If you encounter native crashes, the CLI can forward native logs directly to the console:

ns logs android

These logs include stack traces from the underlying Java/Kotlin or Objective‑C/Swift layers, making it easier to pinpoint the offending native call.

Deploying to the App Stores

When you’re ready to ship, generate signed binaries with a single command. For Android:

ns build android --release --key-store-path ~/mykeystore.jks --key-store-password **** --key-alias myalias --key-password ****

For iOS, you’ll need an Apple Developer certificate and provisioning profile. The CLI can fetch these automatically if you’ve configured fastlane:

ns build ios --release --team-id YOUR_TEAM_ID

After the builds finish, upload the .aab (Android App Bundle) to Google Play Console and the .ipa to App Store Connect. Remember to increment the version code and bundle identifier before each release.

Real‑World Use Cases

Enterprise Field Service: Companies can deliver a single codebase that accesses barcode scanners, GPS, and offline storage, all while preserving the native look required by corporate branding.

Education Apps: Because the UI is native, interactive simulations run smoothly on low‑end devices, and teachers can embed platform‑specific features like ARKit or ARCore without learning a new language.

IoT Dashboards: NativeScript’s ability to call native Bluetooth and BLE APIs lets developers build responsive control panels for smart home devices, all from JavaScript.

Best Practices Checklist

  • Use --bundle for release builds to reduce payload size.
  • Guard all platform‑specific code with global.isIOS / global.isAndroid.
  • Keep XML layouts shallow; deep nesting hurts layout performance.
  • Leverage ObservableArray for list data to enable efficient UI updates.
  • Test on real devices regularly; emulators can hide native quirks.

Conclusion

NativeScript 9 proves that JavaScript can still be a first‑class citizen in native mobile development. By offering direct access to platform APIs, a familiar XML‑CSS UI model, and a powerful CLI, it empowers web developers to ship high‑performance iOS and Android apps without learning a new language. Whether you’re building a camera‑centric social app, a location‑aware logistics tool, or an enterprise field‑service solution, NativeScript gives you the flexibility to go native while staying in the JavaScript ecosystem.

Share this article