# Vibration and sound - JavaScript bridge API

> 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.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/haptics

### `navigator.vibrate`

```js
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.

```js
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:

```js
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:

```js
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`

```js
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:

```js
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":

```js
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:

```js
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`

```js
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:

```js
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):

```js
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:

```js
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`

```js
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.

```js
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:

```js
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:

```js
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`

```js
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:

```js
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:

```js
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):

```js
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`

```js
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):

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

The raw call:

```js
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:

```js
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:

```js
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`

```js
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:

```js
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**:

```js
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:

```js
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.

