Build APK + AAB

JavaScript bridge API

Vibration and sound

Haptic feedback, the app's own click sound fired on your semantic events rather than every stray tap, and the background music track the creator chose at build time - getMusicVolume returns -1 when the app ships none, so a settings screen can hide the slider instead of showing a dead one.

navigator.vibrate web standard#

navigator.vibrate(pattern)

Example

Vibrates the phone, using the normal web Vibration API. Android WebView's own navigator.vibrate never moves the motor; the app replaces it at document start so it really buzzes.

Returns: true when the request was accepted, false for bad input (a negative or non-number duration). Needs: turn on Vibrate in Step 4 (Access) when you build. AI-built apps get it switched on for you when the code vibrates.

function buzz(pattern) {
  if ('vibrate' in navigator) navigator.vibrate(pattern);
}

document.getElementById('save').addEventListener('click', function () {
  buzz(30);                    // one short buzz, 30 ms
});

document.getElementById('wrong').addEventListener('click', function () {
  buzz([80, 40, 80]);          // buzz 80, pause 40, buzz 80
});

document.getElementById('alarm-off').addEventListener('click', function () {
  buzz(0);                     // 0 or [] stops a running vibration
});

Use the return value to catch a bad duration, for example one typed by the user:

document.getElementById('test-buzz').addEventListener('click', function () {
  var ms = parseInt(document.getElementById('buzz-ms').value, 10);   // '' gives NaN
  if (!('vibrate' in navigator)) return;
  if (!navigator.vibrate(ms)) {
    showError('Enter a duration of 0 ms or more.');
  }
});

One short tick when a pull-to-refresh passes the point where letting go will refresh:

var armed = false;
var list = document.getElementById('feed');

list.addEventListener('touchmove', function () {
  var ready = list.scrollTop <= 0 && pullDistance() > 80;
  if (ready && !armed && 'vibrate' in navigator) navigator.vibrate(15);
  armed = ready;
}, { passive: true });

list.addEventListener('touchend', function () {
  if (armed) refreshFeed();
  armed = false;
});

Notes:

  • The pattern is the web one: the first number is a buzz, then pause, buzz, pause ... (the app converts it for Android).
  • If Vibrate was not turned on when you built, the call still returns true but the phone does not vibrate.
  • Decimals are rounded down. In an array, negative numbers count as 0.
  • In a desktop browser navigator.vibrate may be missing, so keep the 'vibrate' in navigator check.

cancelVibrate bridge#

window.WebToApk.cancelVibrate()

Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.

Described by its group, Vibration, rather than on its own.

Example

Stops a vibration that is still running. Most pages use the standard navigator.vibrate(0), which calls this for you.

Returns: nothing. Needs: turn on Vibrate in Step 4 (Access) when you build.

The standard way:

if ('vibrate' in navigator) navigator.vibrate(0);   // or navigator.vibrate([])

The raw call, for example to stop a long alarm pattern when the user taps "Dismiss":

if (window.WebToApk && window.WebToApk.vibratePattern) {
  window.WebToApk.vibratePattern(JSON.stringify([1000, 500, 1000, 500, 1000]));
}

document.getElementById('dismiss').addEventListener('click', function () {
  if (window.WebToApk && typeof window.WebToApk.cancelVibrate === 'function') {
    window.WebToApk.cancelVibrate();
  }
});

Stop a running vibration when the user leaves the screen, so the phone does not keep buzzing in their pocket:

document.addEventListener('visibilitychange', function () {
  if (!document.hidden) return;
  if (window.WebToApk && typeof window.WebToApk.cancelVibrate === 'function') {
    window.WebToApk.cancelVibrate();
  }
});

Notes: Safe to call when nothing is vibrating.

getMusicVolume bridge#

window.WebToApk.getMusicVolume(): Int

Current level 0..100, or -1 when this app ships no background music.

Example

Reads the level of the app's looping background music, 0 to 100.

Returns: a number, synchronously: 0-100, or -1 when this app was built without a music file. Needs: a file chosen under Background Music in Step 2 (Display) when you build.

Show a music slider only when there is music:

var slider = document.getElementById('music-slider');
var level = -1;
if (window.WebToApk && typeof window.WebToApk.getMusicVolume === 'function') {
  level = window.WebToApk.getMusicVolume();
}

if (level < 0) {
  slider.parentNode.hidden = true;          // no music in this app: hide the setting
} else {
  slider.value = level;
  slider.addEventListener('input', function () {
    window.WebToApk.setMusicVolume(parseInt(slider.value, 10));
  });
}

A mute button that returns to the user's own level on unmute (the app remembers 0 too, so keep the old level yourself):

function musicLevel() {
  return window.WebToApk && typeof window.WebToApk.getMusicVolume === 'function'
    ? window.WebToApk.getMusicVolume() : -1;
}
var muteBtn = document.getElementById('mute');

function paint() {
  var v = musicLevel();
  muteBtn.hidden = v < 0;
  muteBtn.textContent = v === 0 ? 'Unmute' : 'Mute';
}

muteBtn.addEventListener('click', function () {
  var v = musicLevel();
  if (v < 0) return;
  if (v > 0) {
    try { localStorage.setItem('musicBefore', String(v)); } catch (e) {}
    window.WebToApk.setMusicVolume(0);
  } else {
    var back = 40;
    try { back = parseInt(localStorage.getItem('musicBefore') || '40', 10); } catch (e) {}
    window.WebToApk.setMusicVolume(back);
  }
  paint();
});
paint();

Fade the music from its current level to a new one, for example under a dialogue scene:

function fadeMusicTo(target, ms) {
  if (!window.WebToApk || typeof window.WebToApk.getMusicVolume !== 'function') return;
  var start = window.WebToApk.getMusicVolume();
  if (start < 0 || start === target) return;
  var steps = 10, i = 0;
  var t = setInterval(function () {
    i++;
    window.WebToApk.setMusicVolume(Math.round(start + (target - start) * i / steps));
    if (i === steps) clearInterval(t);
  }, ms / steps);
}

fadeMusicTo(10, 800);     // dim under the dialogue
// later: fadeMusicTo(60, 800);  the last level set is the one kept for the next launch

Notes: Returns the level set by the build slider or by your last setMusicVolume call. When another app or a call takes the audio, the music ducks or pauses by itself; the number you read does not change.

playClick bridge#

window.WebToApk.playClick()

Plays the app's tap sound on demand, so a page can fire it on its OWN semantic events (a button press) instead of on every stray tap - which is what a native app actually does, and the reason clickSoundMode can be "off" while this still works. A no-op only when the app was built WITHOUT tap sound (enableClickSound off). In clickSoundMode "off" - no automatic every-tap sound - an explicit call still plays: that mode exists for apps that sound on their own events. Otherwise it plays the creator's configured feedback.

Example

Plays the app's tap sound once, so your own buttons can click like a native app.

Returns: nothing. Needs: Tap sound switched on in Step 2 (Display) when you build. With the switch off, this does nothing.

function clickSound() {
  if (window.WebToApk && typeof window.WebToApk.playClick === 'function') {
    window.WebToApk.playClick();
  }
}

document.querySelectorAll('button, .tab').forEach(function (el) {
  el.addEventListener('click', clickSound);
});

One listener for a whole game board, clicking only on a tap that does something:

document.getElementById('board').addEventListener('click', function (e) {
  var cell = e.target.closest('.cell');
  if (!cell || cell.classList.contains('taken')) return;   // a dead tap stays silent
  if (window.WebToApk && typeof window.WebToApk.playClick === 'function') {
    window.WebToApk.playClick();
  }
  placeMark(cell);
});

Give your users their own "Sounds" switch on top of the build setting:

var toggle = document.getElementById('sounds-toggle');
var soundsOn = true;
try { soundsOn = localStorage.getItem('sounds') !== 'off'; } catch (e) {}
toggle.checked = soundsOn;

toggle.addEventListener('change', function () {
  soundsOn = toggle.checked;
  try { localStorage.setItem('sounds', soundsOn ? 'on' : 'off'); } catch (e) {}
});

function tapSound() {
  if (soundsOn && window.WebToApk && typeof window.WebToApk.playClick === 'function') {
    window.WebToApk.playClick();
  }
}

Notes: What you hear depends on the Tap sound choice:

  • Follow phone setting (recommended): the system click. It is silent when the user turned off "Touch sounds" in the phone's settings. That is on purpose.
  • Always play on every tap: the app's own click, always audible. Every tap on the page clicks too, so you usually do not need playClick() in this mode.

Call it from a real button press, not from a timer.

setMusicVolume bridge#

window.WebToApk.setMusicVolume(percent: Int)

Set the background-music level from the page, 0..100. The wizard slider decides how the app STARTS; this is how an app gives its own users a music control, which any game with a settings screen is expected to have. Clamped in [BackgroundMusic.setVolume], so a page passing 500 or -1 cannot hand MediaPlayer a gain outside the range it accepts. The level is REMEMBERED: the next launch starts at it, not at the wizard's level. A no-op when the app was built without a track - there is nothing to set, and the honest answer is [getMusicVolume] returning -1 so the page can hide its slider rather than show one that does nothing.

Example

Changes the level of the app's looping background music, 0 to 100, for example from your game's settings screen.

Returns: nothing. Needs: a file chosen under Background Music in Step 2 (Display) when you build. Without music this does nothing (check getMusicVolume() for -1).

A mute button - the app remembers the level for the next launch:

function hasMusic() {
  return !!(window.WebToApk && window.WebToApk.getMusicVolume) && window.WebToApk.getMusicVolume() >= 0;
}

if (!hasMusic()) document.getElementById('mute').hidden = true;

document.getElementById('mute').addEventListener('click', function () {
  if (!hasMusic()) return;
  // clamped to 0..100 by the app, and kept for the next launch
  window.WebToApk.setMusicVolume(window.WebToApk.getMusicVolume() > 0 ? 0 : 40);
});

Off / Low / High buttons in a settings screen:

var LEVELS = { off: 0, low: 25, high: 70 };

document.querySelectorAll('[data-music]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    if (!window.WebToApk || typeof window.WebToApk.setMusicVolume !== 'function') return;
    if (window.WebToApk.getMusicVolume() < 0) return;       // built without music
    window.WebToApk.setMusicVolume(LEVELS[btn.dataset.music]);
    markSelected(btn);
  });
});

Turn the music down while your own video plays, then put it back (the level is remembered, so always restore it):

var video = document.getElementById('intro-video');
var before = -1;

video.addEventListener('play', function () {
  if (!window.WebToApk || typeof window.WebToApk.getMusicVolume !== 'function') return;
  before = window.WebToApk.getMusicVolume();
  if (before > 0) window.WebToApk.setMusicVolume(Math.round(before / 4));
});

function restoreMusic() {
  if (before > 0) window.WebToApk.setMusicVolume(before);
  before = -1;
}
video.addEventListener('pause', restoreMusic);
video.addEventListener('ended', restoreMusic);

Notes: The level you set is saved: the next launch starts at it instead of the level from the build slider (until the user clears the app's data). 0 makes the music silent but it keeps playing.

vibrate bridge#

window.WebToApk.vibrate(durationMs: Long)

Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.

Described by its group, Vibration, rather than on its own.

Example

Vibrates the phone once for the given number of milliseconds. Most pages use the standard navigator.vibrate(ms), which calls this for you.

Returns: nothing. Needs: turn on Vibrate in Step 4 (Access) when you build. Without it the call does nothing.

The standard way (works the same in a browser):

if ('vibrate' in navigator) navigator.vibrate(50);

The raw call:

document.getElementById('like').addEventListener('click', function () {
  if (window.WebToApk && typeof window.WebToApk.vibrate === 'function') {
    window.WebToApk.vibrate(40);      // 40 ms
  }
});

// 0 (or any value <= 0) stops a running vibration
if (window.WebToApk && window.WebToApk.vibrate) window.WebToApk.vibrate(0);

A tiny tick each time a slider snaps to a new step, like a native picker:

var slider = document.getElementById('size');
var last = slider.value;
slider.addEventListener('input', function () {
  if (slider.value === last) return;          // same step, no tick
  last = slider.value;
  if (window.WebToApk && typeof window.WebToApk.vibrate === 'function') {
    window.WebToApk.vibrate(10);              // 10 ms: a light tick
  }
});

A longer buzz when a form cannot be sent, so the user feels the error:

document.getElementById('signup').addEventListener('submit', function (e) {
  if (this.checkValidity()) return;
  e.preventDefault();
  if (window.WebToApk && typeof window.WebToApk.vibrate === 'function') {
    window.WebToApk.vibrate(250);
  }
  this.reportValidity();
});

Notes: Pass a whole number. The strength is the phone's default; for stronger/weaker taps use Capacitor.Plugins.Haptics.impact or Capacitor.Plugins.AppwrightHaptics.play. For a buzz-pause-buzz pattern use vibratePattern.

vibratePattern bridge#

window.WebToApk.vibratePattern(patternJson: String)

Vibrates an on/off pattern, e.g. "[100,50,100]" = buzz 100ms, pause 50, buzz 100. Backs navigator.vibrate(array); prefer the standard API. No-op unless the creator enabled the Vibrate permission.

Example

Vibrates an on/off pattern, given as a JSON string. Most pages use the standard navigator.vibrate([...]), which calls this for you.

Returns: nothing. Needs: turn on Vibrate in Step 4 (Access) when you build. Without it the call does nothing.

The standard way:

if ('vibrate' in navigator) navigator.vibrate([100, 50, 100]);   // buzz, pause, buzz

The raw call takes the same web-style pattern as a JSON string:

function pattern(list) {
  if (window.WebToApk && typeof window.WebToApk.vibratePattern === 'function') {
    window.WebToApk.vibratePattern(JSON.stringify(list));
  }
}

pattern([200]);                     // one 200 ms buzz
pattern([100, 50, 100, 50, 300]);   // short, short, long
pattern([]);                        // empty list stops a running vibration

The pattern plays once, so an alarm repeats it itself until the user stops it:

var alarm = null;
var ALARM = JSON.stringify([500, 300, 500, 300, 500]);   // about 2.1 s

function startAlarm() {
  if (!window.WebToApk || typeof window.WebToApk.vibratePattern !== 'function') return;
  window.WebToApk.vibratePattern(ALARM);
  alarm = setInterval(function () { window.WebToApk.vibratePattern(ALARM); }, 3000);
}

function stopAlarm() {
  if (alarm === null) return;
  clearInterval(alarm);
  alarm = null;
  window.WebToApk.vibratePattern('[]');                 // stop the buzz that is still running
}

Notes: The first number is a buzz, then pause, buzz, pause ... (web order). The app adds Android's leading "wait" for you, so do not put a 0 first yourself: [0, 100] means "buzz 0 ms, pause 100 ms" and you feel nothing. Negative numbers count as 0. The pattern plays once.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-11-04.