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.
1. Declare the API in WebIDL
Section titled “1. Declare the API in WebIDL”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.
[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
2. Implement it in Kotlin
Section titled “2. Implement it in Kotlin”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.
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()Implement dev.niso.host.PageLifecycle too if the module needs to hear
onResume, onPause and onNewIntent for its page.
3. Give it to the page
Section titled “3. Give it to the 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:
engine = Engine(applicationContext, script, PAGE, modules = listOf(phone))examples/ihatemyphone/android/app/src/main/kotlin/com/oofdere/ihatemyphone/SetupActivity.kt, line 22
4. Import it
Section titled “4. Import it”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.
Build and test
Section titled “Build and test”examples/ihatemyphone/build.sh --installHeadless 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.