Skip to content

Write a native module

Checked against c430ebe on 2026-11-10. The files it covers are unchanged since.

A native module gives your page an API written in Kotlin. ihatemyphone, a volume panel app in examples/ihatemyphone, has one called phone: its Svelte pages read and set the volume, the ringer mode and the assistant setup through it. It takes three files.

Android only. There is no Swift generator yet.

A module is a directory named after it, beside the app, with an IDL file. Mark the interface [NisoModule=<name>]. Operations, attributes, enums, dictionaries, sequences and nullable types convert as WebIDL says; a Promise<T> settles later; an event handler attribute is an event the native side sends.

examples/ihatemyphone/phone/phone.webidl
[NisoModule=phone]
interface Phone {
// The streams the stream button cycles through, in order.
sequence<StreamInfo> streams();
// The stream the volume buttons would change right now.
Stream defaultStream();
double level(Stream stream);
// Sets a level in [min, max] and returns the level applied.
double setLevel(Stream stream, double level);

examples/ihatemyphone/phone/phone.webidl, line 32

The module’s android/build.gradle.kts generates a Java base class from the IDL. Subclass it: each operation and attribute is an abstract method. A promise’s method gets a Resolver to settle from any thread; a DOMException you throw reaches the page as itself, an IllegalArgumentException as a TypeError.

examples/ihatemyphone/phone/android/src/main/kotlin/com/oofdere/ihatemyphone/phone/PhoneModule.kt
override fun streams(): List<StreamInfo> = STREAMS.map { (stream, s) ->
StreamInfo(stream, Volume.label(s), volume.min(s), volume.max(s), volume.isFine(s))
}
override fun defaultStream(): Stream = STREAMS.entries.first { it.value == volume.defaultStream() }.key
override fun level(stream: Stream): Double = volume.level(STREAMS.getValue(stream)).toDouble()
override fun setLevel(stream: Stream, level: Double): Double = volume.setLevel(STREAMS.getValue(stream), level.toFloat()).toDouble()

examples/ihatemyphone/phone/android/src/main/kotlin/com/oofdere/ihatemyphone/phone/PhoneModule.kt, line 63

Implement dev.niso.host.PageLifecycle too if the module needs to hear onResume, onPause and onNewIntent for its page.

Add the module’s Gradle project to the app and pass an instance to the Engine that runs the page. The page gets it before its script runs:

examples/ihatemyphone/android/app/src/main/kotlin/com/oofdere/ihatemyphone/SetupActivity.kt
engine = Engine(applicationContext, script, PAGE, modules = listOf(phone))

examples/ihatemyphone/android/app/src/main/kotlin/com/oofdere/ihatemyphone/SetupActivity.kt, line 22

examples/build.mjs resolves niso:<name> to the IDL file beside the page or above it and writes <name>.d.ts next to it for your editor:

import { phone } from 'niso:phone';
phone.onlevelchange = (stream, level) => { /* ... */ };

Results and events arrive as tasks of the page’s event loop, never in the middle of a script. An event sent while no page has the module is dropped, so read state through an attribute when the page starts.

Terminal window
examples/ihatemyphone/build.sh --install

Headless tests can define a fake module on a dev.niso.dom.Dom with dom.define(fake), as examples/ihatemyphone/test does. Permissions, streams and what’s still missing are in Native modules.