Create Free APK

JavaScript bridge API

Home-screen widget

A small Android card on the home screen that your page fills with a title, a value and a few lines - AppMint.widget.

AppMint.widget helper#

AppMint.widget.set(card) / update(partial) / clear() / placedCount() / requestPin()

Example

Fills your app's home-screen widget: a native card with a title, one big value, a caption, up to twelve lines (each with its own colour, marker and tap route if you like), your colours, and a route that a tap opens. When the user makes the widget bigger it shows more lines, and with textSize: 'auto' the text grows to fill it - as far as every line still fits, a long line taking two rows rather than being cut. A widget cannot show HTML - you send it the text.

Returns: every method returns a Promise. set(card) β†’ {ok: true, placed} (placed = how many copies are on the home screen) or {ok: false, error}. clear() β†’ undefined. placedCount() β†’ a number. requestPin() β†’ true if the launcher's "Add" sheet opened. Needs: nothing - every build has the widget.

Set the card - once at start, and again every time the data it shows changes:

async function updateWidget(events) {
  if (!(window.AppMint && AppMint.widget)) return;          // in a browser
  var r = await AppMint.widget.set({
    title: 'Today',
    value: events.length + ' events',
    lines: events.slice(0, 6).map(function (ev) { return ev.time + ' ' + ev.name; }),
    route: '/calendar',      // a tap opens the app at this route
    bg: '#0E7C86',           // background
    ink: '#FFFFFF',          // title and lines
    accent: '#FFE08A'        // the big value
  });
  if (!r.ok) console.log('Widget not updated: ' + r.error);
}

An agenda that fills the widget - see-through background, text that grows when the widget is resized, coloured lines with a marker, an overdue line in red, and a tap on a line that opens that day:

AppMint.widget.set({
  title: 'Agenda',
  value: 'Sun 18 Oct',
  caption: '5 events, 1 overdue',          // small text under the value
  bg: 'rgba(20, 38, 74, 0.6)',             // 60 % opaque β€” the wallpaper shows through
  ink: '#FFFFFF', accent: '#FFE08A',
  textSize: 'auto',                        // 'auto' | 'small' | 'normal' | 'large' | 'xl' | a number 0.6–3
  align: 'start',                          // 'start' (left) | 'center' | 'end'
  lines: [
    { text: '09:00 Stand-up', marker: '#29B6F6', route: '/#/day/2026-10-18' },
    { text: '10:30 Dentist, Dr Rao', marker: '#43A047', route: '/#/day/2026-10-18' },
    { text: 'Overdue: renew the car insurance', color: '#FF5252', marker: '#FF5252', bold: true, route: '/#/tasks/overdue' },
    { text: 'Tue 20: Team lunch', route: '/#/day/2026-10-20' },
    'Wed 21: Gym'                           // a plain string is still a line
  ],
  route: '/#/today'                         // a tap anywhere else on the card
});

A line is a string or { text, color?, marker?, bold?, wrap?, route? }; marker puts a coloured ● in front of the text, wrap: true on one line (or on the card) lets a long line take two rows at any text size; with textSize: 'auto' lines already do, and wrap: false keeps a line on one row. Up to six lines always show; lines seven to twelve show when the widget is tall enough.

Richer widgets - a list, a big number with a progress bar, a background picture and buttons. A card is drawn this way only when it has one of these keys; a card without them looks exactly as before:

AppMint.widget.set({
  layout: 'list',                 // 'list' | 'progress' | 'card'
  title: 'Today', subtitle: 'Tue 30 Sep',
  items: [                         // up to 5 rows; a row tap opens its own route
    { text: '09:00 Dentist', secondary: 'Dr Rao, 2nd floor', image: 'img/tooth.png', route: '/event/41' },
    { text: '13:30 Lunch with Sam', image: 'https://example.com/sam.jpg', route: '/event/42' }
  ],
  buttons: [{ label: 'Add', route: '/new' }, { label: 'Week', route: '/week' }],   // up to 2
  bgImage: 'img/widget-bg.jpg', bg: '#101820', ink: '#FFFFFF', accent: '#FFB000'
});

AppMint.widget.set({
  layout: 'progress', title: 'Steps', value: '8 214',
  progress: { value: 8214, max: 10000, label: '82% of today's goal' }
});

AppMint.widget.update({ value: '8 950', progress: { value: 8950, max: 10000 } });   // change only these keys

Your own font - put the font file in your app (.ttf or .otf, e.g. from Google Fonts' "Download family") and give its path, as you would an <img>; every text on the card is drawn in it:

AppMint.widget.set({
  title: 'Score', value: '1 250',
  font: 'fonts/RocknRollOne-Regular.ttf',   // a file in your app, an https address, or a data: URL
  lines: ['Level 7', { text: 'New record!', bold: true }]
}).then(function (r) { if (!r.ok) console.log(r.error); });   // e.g. "font file not found in the app: …"

A font NAME such as 'RocknRoll One' cannot work: the home screen draws widgets, and it has only the phone's own fonts - set() answers {ok: false, error} saying so. Those built-in families need no file: 'serif', 'monospace', 'sans-serif-condensed', 'sans-serif-light', 'sans-serif-medium', 'sans-serif-black', 'casual', 'cursive', 'sans-serif-smallcaps'. WOFF / WOFF2 files are refused (Android cannot read them) - use the .ttf. With a font file the card is drawn as a picture at the widget's size and redrawn when it is resized; taps on lines, rows and buttons and screen readers work as on any card.

Refresh with the app closed - give a https address that returns the same JSON; Android fetches it every refreshMinutes (30 or more, default 60) while a widget is on the home screen, even after a restart:

AppMint.widget.set({
  title: 'Orders', value: '…',
  refreshUrl: 'https://api.example.com/widget.json?user=42',
  refreshMinutes: 30
}).then(function (r) { console.log('refreshing every ' + r.refreshMinutes + ' min'); });
// widget.json answers e.g. {"title":"Orders","value":"12 new","layout":"list","items":[{"text":"#1042 paid"}]}

Images: a path in your app (resolved like an <img src> in the page), an https address, or a data:image/… URL; they are scaled down and kept on the phone. In the refresh JSON a relative path means a file of the app. refreshUrl: '' stops the refresh; clear() stops it too. Android decides the exact moment of each refresh (it batches background work to save battery).

An "Add to home screen" button:

async function addWidget() {
  if (!(window.AppMint && AppMint.widget)) return;
  if ((await AppMint.widget.placedCount()) > 0) { alert('The widget is already on your home screen'); return; }
  var opened = await AppMint.widget.requestPin();
  if (!opened) alert('Touch and hold your home screen, tap Widgets, and pick this app.');
}

Empty it (for example after the user signs out):

if (window.AppMint && AppMint.widget) AppMint.widget.clear();   // shows "Nothing to show yet"

A tap on the widget opens the app with appmint-shortcut://<package><route> as its launch link - read it with WebToApk.getLaunchUrl() on start and the appmint:deep-link event while running:

window.addEventListener('appmint:deep-link', function (e) {
  var m = /^appmint-shortcut:\/\/[^\/?#]*([^#]*)/i.exec(e.detail.url);
  if (m) goTo(m[1] || '/');
});

Colours: #RRGGBB, #RGB, rgb(20, 38, 74), rgba(20, 38, 74, 0.6) (also rgb(20 38 74 / 60%)), transparent and plain names such as red. Eight-digit hex is Android order, alpha FIRST: #AARRGGBB. #9914264A is 60 % opaque; CSS's #14264A99 reads here as alpha 14 (8 %), an almost invisible card. For see-through backgrounds rgba() avoids the confusion. Line colours (color, marker) take the same forms; a bad line colour is refused with {ok: false, error: 'lines[2].color is not a colour'}, a bad card colour uses the default.

Notes: All card fields are optional: an empty title shows the app name; empty value, caption or lines are hidden; lines after the twelfth are dropped. textSize and align apply to every text on the card; an unknown value is refused with {ok: false, error}. A card without the new keys (caption, textSize, align, wrap, styled lines) looks exactly as before. The widget keeps the last card, even after a restart, and changes only when your page calls set() again. requestPin() needs Android 8+ and a launcher that supports it. AI-built apps can use widget from @/lib/appmintNative. Styled lines, caption, textSize, align, wrap and rgba() need an app built with AppMint 1.11.38 or newer - rebuild the app before you send line objects, because an older build would show them as raw text. font needs an app built with AppMint 1.11.41 or newer (an older build ignores it). Website-mode apps: a tap opens the route on your website - /#/today loads https://<your site>/#/today - and getLaunchUrl() still returns the appmint-shortcut:// link.

widgetClear bridge#

window.WebToApk.widgetClear()

Removes the card; placed widgets go back to their empty state.

Example

Internal plumbing behind AppMint.widget.clear() - removes the widget's card; every placed widget goes back to "Nothing to show yet".

Returns: nothing. Needs: nothing.

The public API first:

async function onSignOut() {
  if (window.AppMint && AppMint.widget) await AppMint.widget.clear();
}

The raw call:

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

Tie the widget to a "Show on home screen" switch in your settings - off empties it, on fills it again:

document.getElementById('widget-toggle').onchange = async function (e) {
  if (!(window.AppMint && AppMint.widget)) return;
  if (e.target.checked) {
    await AppMint.widget.set(buildWidgetCard()); // your own card builder
  } else {
    await AppMint.widget.clear();
  }
};

After deleting the account, empty the widget and tell the user if a copy is still on their home screen:

function afterAccountDeleted() {
  var W = window.WebToApk;
  if (!W || typeof W.widgetClear !== 'function') return;
  W.widgetClear();
  if (W.widgetPlacedCount() > 0) {
    showMessage('Your widget is now empty. You can remove it from your home screen.');
  }
}

Notes: The widget stays on the home screen; only its content is removed. Call AppMint.widget.set() to fill it again.

widgetPlacedCount bridge#

window.WebToApk.widgetPlacedCount(): Int

How many copies of the widget the user has placed on the home screen.

Example

Internal plumbing behind AppMint.widget.placedCount() - how many copies of your widget the user has put on the home screen.

Returns: a number, synchronously (0 when none). Needs: nothing.

The public API first - show an "Add widget" button only when the widget is not placed yet:

async function showAddWidgetButton() {
  var btn = document.getElementById('add-widget');
  if (!(window.AppMint && AppMint.widget)) { btn.hidden = true; return; }
  btn.hidden = (await AppMint.widget.placedCount()) > 0;
}

The raw call:

if (window.WebToApk && typeof window.WebToApk.widgetPlacedCount === 'function') {
  var n = window.WebToApk.widgetPlacedCount();
  console.log('Widgets on the home screen: ' + n);
}

Check again each time the user comes back from the launcher, and thank them once the widget is there:

document.addEventListener('visibilitychange', async function () {
  if (document.visibilityState !== 'visible') return;
  if (!(window.AppMint && AppMint.widget)) return;
  var n = await AppMint.widget.placedCount();
  document.getElementById('widget-promo').hidden = n > 0;
  if (n > 0) document.getElementById('widget-thanks').hidden = false;
});

Show the widget's status on a settings page with the synchronous raw call:

var status = document.getElementById('widget-status');
if (window.WebToApk && typeof window.WebToApk.widgetPlacedCount === 'function') {
  var n = window.WebToApk.widgetPlacedCount();
  status.textContent = n === 0 ? 'Not on your home screen'
    : n === 1 ? 'On your home screen' : 'On your home screen ' + n + ' times';
} else {
  status.textContent = 'Available in the installed app';
}

Notes: Check it again when the app comes back to the front (visibilitychange), since the user adds widgets from the launcher, outside your app.

widgetRequestPin bridge#

window.WebToApk.widgetRequestPin(): Boolean

Opens the launcher's "Add to home screen" sheet for the widget (Android 8+). False where the launcher cannot.

Example

Internal plumbing behind AppMint.widget.requestPin() - opens the launcher's "Add to home screen" sheet for your widget.

Returns: true if the sheet opened, false where the launcher cannot (synchronously). Needs: Android 8+ and a launcher that supports pinning widgets.

The public API first, from a button tap:

document.getElementById('add-widget').onclick = async function () {
  if (!(window.AppMint && AppMint.widget)) return;
  var opened = await AppMint.widget.requestPin();
  if (!opened) {
    alert('Touch and hold your home screen, tap Widgets, then pick this app.');
  }
};

The raw call:

if (window.WebToApk && typeof window.WebToApk.widgetRequestPin === 'function') {
  if (!window.WebToApk.widgetRequestPin()) showManualSteps();
}

Fill the card first, then open the sheet, so the widget shows real content the moment it lands:

document.getElementById('add-widget').onclick = async function () {
  if (!(window.AppMint && AppMint.widget)) return;
  await AppMint.widget.set({ title: 'Streak', value: '12 days', lines: ['Best 30'], route: '/streak' });
  if (!(await AppMint.widget.requestPin())) {
    document.getElementById('widget-help').hidden = false; // manual steps
  }
};

Find out whether the user accepted: compare the placed count before the sheet and after they come back:

document.getElementById('add-widget').onclick = function () {
  var W = window.WebToApk;
  if (!W || typeof W.widgetRequestPin !== 'function') return;
  var before = W.widgetPlacedCount();
  if (!W.widgetRequestPin()) { showManualSteps(); return; }
  document.addEventListener('visibilitychange', function check() {
    if (document.visibilityState !== 'visible') return;
    document.removeEventListener('visibilitychange', check);
    if (W.widgetPlacedCount() > before) showToast('Widget added');
  });
};

Notes: true only means the sheet opened - the user can still cancel it. Use AppMint.widget.placedCount() later to see whether it was added. Fill the card with AppMint.widget.set() first, so the new widget is not empty.

widgetSet bridge#

window.WebToApk.widgetSet(cardJson: String): String

Saves the widget card [cardJson] `{title?, value?, lines?, route?, bg?, ink?, accent?}` and redraws every placed widget. Returns `{ok, placed}`.

Example

Internal plumbing behind AppMint.widget.set(card) - pages use that helper to fill the home-screen widget.

Returns: a JSON string {"ok":true,"placed":<number>} or {"ok":false,"error":"…"} (synchronously). Needs: nothing - every build has the widget.

The public API first:

if (window.AppMint && AppMint.widget) {
  AppMint.widget.set({ title: 'Steps', value: '8 214', lines: ['Goal 10 000'], route: '/' })
    .then(function (r) { if (!r.ok) console.log(r.error); });
}

The raw call takes the card as a JSON string and returns JSON text:

if (window.WebToApk && typeof window.WebToApk.widgetSet === 'function') {
  var card = { title: 'Steps', value: '8 214', lines: ['Goal 10 000'], route: '/',
               bg: '#15161A', ink: '#FFFFFF', accent: '#22D3EE' };
  var r = JSON.parse(window.WebToApk.widgetSet(JSON.stringify(card)));
  console.log(r.ok ? 'on ' + r.placed + ' home screen(s)' : r.error);
}

Keep the widget in step with a to-do list - the open count as the value, the first six tasks as lines, and a tap opens the tasks screen:

async function syncTaskWidget(tasks) {
  if (!(window.AppMint && AppMint.widget)) return;
  var open = tasks.filter(function (t) { return !t.done; });
  var r = await AppMint.widget.set({
    title: 'To do',
    value: open.length ? open.length + ' left' : 'All done',
    lines: open.slice(0, 6).map(function (t) { return 'β€’ ' + t.title; }),
    route: '/tasks'
  });
  if (!r.ok) console.warn('Widget not updated: ' + r.error);
}

Write the latest state when the user leaves the app; the raw call is synchronous, so it finishes before the page is paused:

document.addEventListener('visibilitychange', function () {
  if (document.visibilityState !== 'hidden') return;
  if (!window.WebToApk || typeof window.WebToApk.widgetSet !== 'function') return;
  var s = getTodayStats(); // your app's own numbers
  window.WebToApk.widgetSet(JSON.stringify({
    title: 'Today', value: s.spent + ' spent',
    lines: ['Budget ' + s.budget, 'Left ' + (s.budget - s.spent)], route: '/budget'
  }));
});

Notes: Card keys: title, value, caption, lines (up to twelve; a line is a string or {text, color, marker, bold, wrap, route}), route (a / is added if missing), bg, ink, accent, textSize, align, wrap, font (a .ttf/.otf file in your app or a system family such as serif), plus the rich-layout keys - see AppMint.widget. Colours: #RRGGBB, #AARRGGBB (alpha first), rgba(). Every placed widget redraws at once. placed is 0 when the user has not added the widget yet - the card is still saved and appears when they do.

widgetUpdate bridge#

window.WebToApk.widgetUpdate(partialJson: String): String

Merges [partialJson] into the saved widget card (named keys replace, the rest stay) and redraws. Returns `{ok, placed}`.

Example

Changes part of the home-screen widget: the keys you pass replace the saved ones, everything else stays. Pages normally call AppMint.widget.update(partial).

Returns: a JSON string {"ok":true,"placed":<n>} or {"ok":false,"error":"…"}. Needs: nothing - every build has the widget.

// Only the number and the progress bar change; title, rows and buttons stay as they were.
if (window.AppMint && AppMint.widget) {
  AppMint.widget.update({ value: '7 420', progress: { value: 7420, max: 10000, label: '74% of goal' } });
}

// Raw form:
if (window.WebToApk && typeof window.WebToApk.widgetUpdate === 'function') {
  var r = JSON.parse(window.WebToApk.widgetUpdate(JSON.stringify({ subtitle: 'Updated ' + new Date().toLocaleTimeString() })));
  if (!r.ok) console.warn(r.error);
}

Clear the card on sign-out and report an error from update:

async function onSignOut() {
  if (!(window.AppMint && AppMint.widget)) return;
  await AppMint.widget.clear();
}

async function bumpCount(n) {
  if (!(window.AppMint && AppMint.widget)) return;
  const r = await AppMint.widget.update({ value: String(n) });
  if (!r.ok) console.warn('Widget not updated: ' + r.error);
}

Combine it with the placement check: only refresh the card while a copy is on the home screen.

async function refreshWidget(stats) {
  if (!(window.AppMint && AppMint.widget)) return;
  if (await AppMint.widget.placedCount() === 0) return;
  AppMint.widget.update({ value: stats.total + ' today', subtitle: 'Updated ' + new Date().toLocaleTimeString() });
}

Notes: With no saved card, update() behaves like set(). See AppMint.widget for every card key.

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.