Documentation

Handle files, folders, permissions, notifications, badges, and menu bar apps.

HTML to App can register your generated macOS app as a Finder handler for selected file extensions or folders, preselect app permissions from HTML metadata, and pass opened items into your page JavaScript with scoped read and write access where appropriate. Generated apps can also send native macOS notifications, update badges, and run from a JavaScript-controlled menu bar icon.

What the feature does

Use this when you want the generated app to behave like an image viewer, video player, audio player, folder browser, or another file-driven tool.

Open With registration

  • Choose whether the app should open files, folders, or both.
  • Set the role to Viewer or Editor.
  • Enter file extensions such as jpg, png, or mp4.
  • Export the app bundle with those document types included in the generated Info.plist.

Runtime delivery

  • When the user opens a matching file from Finder, macOS launches your generated app.
  • The launcher receives the opened file or folder URLs on the native side.
  • HTML to App forwards those items into the page after the web content is ready.
  • Editor-role apps can also write back inside the opened file or folder scope.
  • The same page can also receive later open events while the app is already running.

App permissions

  • Advanced Settings can enable generated-app entitlements for camera, microphone, or location services.
  • The same permissions can be preselected automatically from HTML metadata when the source is chosen.
  • This is useful for camera viewers, audio capture tools, voice utilities, mapping tools, or mixed-media apps.

Notifications and Dock badge

  • Use the standard Notification API to show native macOS notifications from local HTML.
  • Use window.HTMLtoApp.setBadge(value) to set a Dock tile badge string.
  • Use window.HTMLtoApp.clearBadge() when there is no active count or status to show.
  • The bridge namespace is exactly window.HTMLtoApp.

Menu bar apps

  • Create a menu bar icon from JavaScript with window.HTMLtoApp.menuBar.setIcon().
  • Regenerate the menu on every popup with window.HTMLtoApp.menuBar.setMenu(() => items).
  • Use nested submenu arrays for recursive menus and checked plus toggle for stateful items.
  • Enable closeToMenuBarOnWindowClose so closing the window keeps the app alive in the menu bar.

Auto-detect Open With and permissions

If your main HTML file already knows what kind of app it is, you can declare that in the document head and HTML to App will prefill the Open With controls and Advanced Settings permission checkboxes when the source is selected.

Recommended meta tag

<meta
  name="htmltoapp:open-with"
  content="role=editor; files=jpg,jpeg,png; folders=true; permissions=camera,microphone"
>

Supported values

  • role=viewer or role=editor
  • files=jpg,jpeg,png for file extensions
  • folders=true or folders=false
  • permissions=camera,microphone,location for generated-app entitlements
  • camera=true, microphone=true, and location=true are also accepted
  • mic, audio, geolocation, and gps are accepted aliases
  • The same metadata can also be written as an HTML comment if you prefer.

Example app types

  • Image viewer: role=viewer; files=jpg,jpeg,png,webp; folders=true
  • Video player: role=viewer; files=mp4,mov,m4v; folders=true
  • Folder browser: role=viewer; folders=true
  • Drawing app: role=editor; files=jpg,jpeg,png,webp
  • Camera viewer: permissions=camera

JavaScript API

Generated apps expose launch items, runtime capability info, a scoped file bridge for document-style workflows, and native app-state controls for Dock badges, menu bar icons, and window behavior.

Bridge namespaces

  • window.HTMLtoApp: Finder launch items, runtime information, scoped file operations, Dock badge control, menu bar controls, and window controls.
  • Notification: standard browser-style notification constructor replaced by the generated app so local HTML can request native macOS notifications.

Initial launch items

Read the initial selection from window.HTMLtoApp.launchItems.

const launchItems = (window.HTMLtoApp && window.HTMLtoApp.launchItems) || [];

Later open events

Listen for htmltoapp-open when the user opens more files or folders while the app is already running.

window.addEventListener("htmltoapp-open", (event) => {
  const detail = event.detail || {};
  const items = detail.items || [];
  const replaceExisting = !!detail.replaceExisting;
  handleLaunchItems(items, replaceExisting);
});

Runtime capabilities

Check whether Open With is enabled, whether the build is a viewer or editor, and whether write-back is available through window.HTMLtoApp.runtime.

const runtime = (window.HTMLtoApp && window.HTMLtoApp.runtime) || {
  enabled: false,
  role: "viewer",
  allowFiles: false,
  allowFolders: false,
  canWriteBack: false
};

window.addEventListener("htmltoapp-runtime", (event) => {
  console.log("runtime", event.detail);
});

Notifications and Dock Badge

Use these APIs for local app status such as completed tasks, reminders, unread counts, queue lengths, or attention states. This is native notification passthrough from the generated Mac app; it does not configure APNs or server-delivered remote push notifications.

Send a macOS notification

Check for the API, request permission, then create a notification. On supported macOS releases, HTML to App forwards the request to the generated app's native notification handler.

async function sendAppNotification(title, body) {
  if (!("Notification" in window)) return false;

  const permission = await Notification.requestPermission();
  if (permission !== "granted") return false;

  new Notification(title, {
    body,
    tag: "html-to-app-status"
  });
  return true;
}

Set or clear the Dock badge

Badge values are strings. Numbers work well for counts, while short labels can represent states like paused, new, or sync pending. Request notification permission before enabling Dock badge controls, because macOS ties Dock badge visibility to notification authorization for the generated app.

const appBridge = window.HTMLtoApp;

async function setUnreadCount(count) {
  if (!appBridge || typeof appBridge.setBadge !== "function") return;
  if (!("Notification" in window)) return;

  const permission = await Notification.requestPermission();
  if (permission !== "granted") return;

  if (count > 0) {
    appBridge.setBadge(String(count));
  } else {
    appBridge.clearBadge();
  }
}

Behavior notes

  • Notifications are available through the generated app bridge on macOS versions that support native user notifications.
  • The exported app asks macOS for notification authorization when needed; if the user denies it, the native notification is not delivered.
  • Dock badge controls should use the same permission gate as native notifications, so users are not offered badge actions that macOS will keep hidden.
  • The notification title and body option become the main native notification content.
  • The Dock badge belongs to the running generated app and is cleared when you call clearBadge() or set an empty value.
  • Guard both APIs when the same HTML is also opened in a normal browser.

Menu Bar and Window Controls

Use these APIs for utilities that should keep running from the menu bar, refresh their menu contents from current page state, or expose background task status outside the main window. The generated app icon is shown by default; provide a title to show icon and text together.

Dynamic menu bar menu

The native launcher calls your JavaScript menu provider each time the menu bar icon is clicked. Return a fresh array so the menu reflects current app state.

const appBridge = window.HTMLtoApp;
let paused = false;

appBridge?.menuBar?.setIcon({
  title: "TA",
  tooltip: "Task Agent",
  imagePosition: "left",
  closeToMenuBarOnWindowClose: true
});

appBridge?.menuBar?.setBadge("4");
// Later, remove it and allow the user to start over.
// appBridge?.menuBar?.hideIcon?.();

appBridge?.menuBar?.setMenu(() => [
  { id: "open", title: "Open Window" },
  { type: "separator" },
  { id: "paused", title: "Paused", checked: paused, toggle: true },
  {
    title: "Queues",
    submenu: [
      { id: "inbox", title: "Inbox" },
      { id: "done", title: "Done" }
    ]
  },
  { type: "separator" },
  { id: "quit", title: "Quit" }
]);

appBridge?.menuBar?.onItemClick((event) => {
  if (event.id === "open") appBridge.showWindow();
  if (event.id === "paused") paused = event.checked;
  if (event.id === "quit") appBridge.quit();
});

Window title and visibility

Update the native window title from JavaScript, hide or restore the window, and use a menu item callback to bring a background menu bar app forward again.

window.HTMLtoApp?.setWindowTitle?.("Syncing 4 files");
window.HTMLtoApp?.hideWindow?.();
window.HTMLtoApp?.showWindow?.();

Status item options

  • title: optional text shown next to the generated or supplied image.
  • image: optional data URL, base64 image, or file URL. If omitted, the generated app icon is used.
  • imageVisible: false: hides the image for a text-only menu bar item.
  • imagePosition: "left" or "right": controls which side of the title the image appears on.
  • menuBar.setBadge(value): draws a badge on the image icon. Text-only status items do not append the badge to the title.
  • menuBar.hideIcon() or menuBar.configure({ visible: false }): removes the menu bar item so the user can recreate it from a clean state.

Menu item shape

  • id: stable identifier delivered back in click callbacks.
  • title: visible menu item label.
  • type: "separator": inserts a native separator.
  • enabled: set to false to disable an item.
  • checked or state: "on": shows a checked state.
  • toggle: true: click callbacks receive the next checked value.
  • submenu or items: nested arrays for recursive submenus.

Background behavior

  • closeToMenuBarOnWindowClose only changes close behavior when the menu bar icon is visible.
  • When enabled, closing the generated app window hides the window and Dock icon while the WebView keeps running.
  • The generated app shows a one-time native message explaining that it is still running in the menu bar.
  • Expose hide/show window controls only after the menu bar item exists, so the user has a visible restore path.
  • Use window.HTMLtoApp.showWindow() and window.HTMLtoApp.quit() from menu item callbacks for predictable restore and quit commands.

Launch item shape

Files and folders share a common shape, with a few extra fields depending on item type.

Common fields

  • id: opened-item identifier on root launch items
  • itemId: same identifier, also included on folder listing entries
  • name: display name of the file or folder
  • path: original macOS path as a string
  • relativePath: path inside the opened folder, or an empty string for the root item
  • isDirectory: true for folders, false for files
  • kind: file or directory on root launch items

File and folder extras

  • url: loadable URL for a file item, usable in img, video, audio, or fetch()
  • baseURL: base URL for a folder item
  • listingURL: JSON endpoint for the folder contents

Example folder listing

Folder listing entries use the same basic shape and can be passed back into window.HTMLtoApp.fs through entry.itemId plus entry.relativePath.

const folder = launchItems.find((item) => item.isDirectory);
const listing = await window.HTMLtoApp.fs.list(folder.id);
const entries = Array.isArray(listing.entries) ? listing.entries : [];

Scoped file bridge

The generated app can read opened content in both viewer and editor mode. Mutating methods are only available when Open With is enabled and the role is Editor.

Read and overwrite the opened file

If the launched item is a file, omit relativePath and write directly back to that same file.

const fileItem = launchItems.find((item) => !item.isDirectory);
const opened = await window.HTMLtoApp.fs.readText(fileItem.id);
editor.value = opened.text;

await window.HTMLtoApp.fs.writeText(fileItem.id, editor.value);

Create, replace, move, and remove inside an opened folder

If the launched item is a folder, target descendants by relative path. The bridge never grants access outside that opened folder tree.

const folder = launchItems.find((item) => item.isDirectory);

await window.HTMLtoApp.fs.createDirectory(folder.id, "drafts");
await window.HTMLtoApp.fs.writeText(folder.id, "# Notes\n", "drafts/today.md");
await window.HTMLtoApp.fs.move(folder.id, "drafts/today.md", "archive/today.md", {
  createIntermediates: true,
  overwrite: true
});

Available methods

  • stat(itemOrId, relativePath?) returns metadata for a file or folder.
  • list(itemOrId, relativePath?) returns folder metadata plus entries.
  • readText(itemOrId, relativePath?) returns decoded text plus detected encoding.
  • readData(itemOrId, relativePath?) returns Base64 data plus MIME type.
  • writeText(itemOrId, text, relativePath?, options?) creates or replaces a text file.
  • writeData(itemOrId, base64Data, relativePath?, options?) creates or replaces any file.
  • createDirectory(itemOrId, relativePath, options?) creates a folder inside an opened folder item.
  • remove(itemOrId, relativePath?, options?) removes a file or folder. Use recursive: true for non-empty folders.
  • move(itemOrId, fromRelativePath, toRelativePath, options?) renames or moves items inside an opened folder.

Write options

  • writeText supports encoding, atomic, and createIntermediates.
  • writeData supports atomic and createIntermediates.
  • createDirectory supports createIntermediates.
  • remove supports recursive for non-empty folders.
  • move supports createIntermediates and overwrite.

Recommended handling pattern

Use one shared function for startup items, later open events, and any drag-and-drop path in your page.

function extensionOf(name) {
  const dot = name.lastIndexOf(".");
  return dot === -1 ? "" : name.slice(dot + 1).toLowerCase();
}

function isSupportedFile(item) {
  return !item.isDirectory && SUPPORTED_EXTENSIONS.has(extensionOf(item.name || item.path || ""));
}

async function expandLaunchItems(items) {
  const results = [];

  for (const item of items) {
    if (!item) continue;

    if (isSupportedFile(item)) {
      results.push(item);
      continue;
    }

    if (item.isDirectory && window.HTMLtoApp && window.HTMLtoApp.fs) {
      const listing = await window.HTMLtoApp.fs.list(item.id);
      const entries = Array.isArray(listing.entries) ? listing.entries : [];
      results.push(...entries.filter(isSupportedFile));
    }
  }

  return results;
}

Important behavior notes

The Open With role and file registration are scoped on purpose. They can support real editor workflows without turning the app into an unrestricted filesystem client.

Viewer vs editor

  • Viewer and Editor control how the app is registered with Finder and Launch Services.
  • Viewer keeps the bridge read-only.
  • Editor enables window.HTMLtoApp.fs write methods for the opened item scope only.
  • Use Viewer for galleries, players, and browsers.
  • Use Editor when the app should overwrite the opened file or manage files inside the opened folder.

Source HTML vs opened content

  • HTML to App still exposes loadable item URLs for media tags, image previews, and fetch().
  • Folder items can be expanded through window.HTMLtoApp.fs.list() or the provided listing URL.
  • When you export in external-source mode, the selected HTML source folder remains read-only.
  • Opened document files and folders are a separate scope, and editor-role apps can modify only that specific opened file or folder tree.

Example projects

Start from working demos instead of building the bridge integration from scratch.