All guides
Advanced·7 min read

Build a custom HTML widget with Tauri

When the visual editor isn't enough, build a widget from your own HTML, CSS, and JavaScript. This guide covers loading an HTML widget, pulling live data with native Tauri commands, reacting to events, and making the window draggable.

When to use an HTML widget

HTML widgets run in a plain WebView, so the visual editor's {{...}} variables are not available. Instead you call Tauri commands and subscribe to events yourself. Reach for HTML when you want full control over layout and behavior, or want to reuse an existing web app.

1. Create the widget folder

Make a folder containing an index.html. In Delta Widgets, click Add → HTML and select that folder. Your page is served locally and rendered as a widget.

2. Quick prototype with raw HTML

For static displays or experiments, you can call commands through the global window.__TAURI__ object — no build step required:

<!DOCTYPE html>
<html>
  <body>
    <pre id="out"></pre>
    <script>
      async function load() {
        const info = await window.__TAURI__.core.invoke("get_system_info");
        document.getElementById("out").textContent = JSON.stringify(info, null, 2);
      }
      load();
    </script>
  </body>
</html>

Two caveats: window.__TAURI__ becomes available a moment after load, and it can disappear if the page reloads or redirects. That's fine for prototypes, but fragile for anything you rely on.

3. Recommended: bundle with a build tool

For a real widget, scaffold with a bundler so the typed @tauri-apps/api package is available at runtime:

npm create vite@latest my-widget -- --template vanilla-ts
cd my-widget
npm install
import { invoke } from "@tauri-apps/api/core";

const info = await invoke("get_system_info");
console.log(info.cpu, info.used_memory, info.total_memory);

Run npm run build and drop the generated dist/ folder into Delta Widgets as your HTML widget. React, Vue, Svelte, and Solid all work the same way — scaffold with their CLI and point Delta Widgets at the build output.

4. Show live media

Start the media listener once, read the current media, then refresh whenever it changes:

import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";

await invoke("start_media_listener_cmd");
render(await invoke("get_media"));

await listen("media_updated", async () => {
  render(await invoke("get_media"));
});

get_media returns an array of media sessions. Album art (thumbnail) comes back as a byte array — convert it to a base64 data URI before using it as an image src:

const src = `data:image/png;base64,${Buffer.from(thumbnail).toString("base64")}`;

A player's icon field is a local file path; use convertFileSrc from @tauri-apps/api/core to display it.

5. Add an audio visualizer

Call start_audio_capture to begin receiving audio-samples events — roughly one every 33 ms, each carrying ~256 waveform samples:

await invoke("start_audio_capture");
await listen("audio-samples", (event) => {
  draw(event.payload); // number[] of samples
});

Continuous capture uses CPU, so call stop_audio_capture when the samples are no longer needed.

6. Make the window draggable

WebView widgets have no title bar by default. Mark any element as a drag handle with -webkit-app-region: drag, and keep interactive areas clickable with no-drag:

.titlebar {
  -webkit-app-region: drag;
  cursor: move;
}
.content {
  -webkit-app-region: no-drag; /* buttons and links stay clickable */
}

Command reference

CommandPurpose
get_system_infoCPU, memory, disks, batteries, network
start_media_listener_cmd / stop_media_listener_cmdEnable/disable media_updated events
get_mediaCurrent media sessions
media_actionPlay, pause, toggle, next, prev, seek
start_audio_capture / stop_audio_captureEnable/disable audio-samples events

See the documentation for the full parameter and return-type details.

Next steps

  • Turn this into a full now-playing media widget with controls.
  • Prefer no code? Build a live clock in the visual editor instead.