npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tauri-plugin-widgets-api

v0.3.0

Published

A Tauri v2 plugin to interact with App Widgets on Android and iOS, with desktop fallback. SharedPreferences / UserDefaults / WidgetKit / AppWidgetManager.

Readme

npm version Crates.io Documentation GitHub issues GitHub stars Donate

Tauri Plugin Widgets

A Tauri v2 plugin for building native widgets on Android, iOS and macOS, with desktop widget windows on Windows and Linux. Includes a universal JSON-based UI generator — send a single config from your Tauri app and the plugin renders it natively on every platform (SwiftUI on Apple, Jetpack Glance on Android, HTML on desktop).

Crates.io npm MIT licensed

Demo

Preview


Architecture

The plugin acts as a library, not a builder:

| Component | Role | |-----------|------| | Rust plugin (src/) | Data storage, FFI bridge, WidgetKit reload commands | | Swift Package (swift/TauriWidgets) | Cross-platform SwiftUI views, models, data store for iOS/macOS widgets | | Android plugin (android/) | Jetpack Glance renderer, GlanceAppWidgetReceiver, BroadcastReceiver | | Desktop (widget.html) | HTML/CSS renderer for transparent webview widget windows | | Templates (templates/) | Starter files for iOS and macOS widget extensions |

Data flow: Your Tauri app calls setWidgetConfig(json, group) → plugin writes JSON to the App Group shared container → calls WidgetCenter.reloadAllTimelines() → native widget extension reads JSON and renders SwiftUI views.

The developer owns the widget extension target — the plugin provides reusable SwiftUI components via the TauriWidgets Swift Package.


Features

  • Universal Widget UI — describe widgets as JSON, render natively on all platforms.
  • Three size familiessmall, medium, large layouts in a single config.
  • 21 element types — text, image, progress, gauge, chart, list, button, toggle, divider, spacer, date, link, shape, timer, label, canvas, and layout containers (vstack, hstack, zstack, grid, container).
  • Action buttons & tappable wrappers — buttons and link elements can emit widget-action Tauri events back to the main app, enabling two-way communication.
  • Dark mode & adaptive colors — semantic color names ("label", "systemBackground", "accent") and adaptive { light, dark } color objects auto-switch with the system theme.
  • Semantic typographytextStyle property ("largeTitle", "body", "caption", etc.) respects platform Dynamic Type / accessibility settings.
  • Flexible layoutflex property for proportional space distribution, clipShape for content masking (circle, capsule, rectangle).
  • Full styling — padding, gradient backgrounds, corner radius, opacity, borders, shadows, frames.
  • Declarative canvas — draw arbitrary shapes (circles, lines, arcs, paths) via JSON commands.
  • Swift Package — reusable TauriWidgets package for iOS/macOS widget extensions.
  • File-based data sharing — JSON file in App Group shared container (reliable, atomic, sandbox-safe).
  • Configurable reload throttling — WidgetKit reload rate-limit with sensible defaults and TAURI_WIDGET_MIN_RELOAD_SECS override.
  • Desktop widget windows — frameless, transparent Tauri webview windows for Windows/Linux.
  • Low-level data APIsetItems/getItems for arbitrary key-value storage shared with native widgets.

Recent Android Updates

  • Android rendering path migrated to Jetpack Glance.
  • Example presets were simplified to avoid over-nested layout trees that can render inconsistently on some launchers.
  • Complex container wrappers were reduced in favor of flatter vstack/hstack structures.
  • Progress examples now include explicit labels to avoid host text fallbacks like null.

Platform Support

| Platform | Native Widget | UI Generator | Data Storage | Reload | |----------|--------------|-------------|-------------|--------| | Android | AppWidget | Jetpack Glance from JSON | SharedPreferences + Glance state | Glance updateAll() | | iOS | WidgetKit (17+) | SwiftUI from JSON | App Group shared container (JSON file) | WidgetCenter | | macOS | WidgetKit (14+) | SwiftUI from JSON | App Group shared container (JSON file) | WidgetCenter | | Windows | Desktop window | HTML/CSS from JSON | JSON file | Tauri event | | Linux | Desktop window | HTML/CSS from JSON | JSON file | Tauri event |


Quick Start

1. Install

# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-widgets = "0.3"
npm install tauri-plugin-widgets-api
# or
pnpm add tauri-plugin-widgets-api

2. Register the Plugin

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_widgets::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

3. Add Permissions

src-tauri/capabilities/default.json:

{
  "permissions": [
    "core:default",
    "widgets:default"
  ]
}

4. Send a Widget Config

import { setWidgetConfig } from "tauri-plugin-widgets-api";

await setWidgetConfig({
  small: {
    type: "vstack",
    padding: 14,
    background: { light: "#E8F4FD", dark: "#1a1a2e" },
    spacing: 6,
    cornerRadius: 16,
    children: [
      {
        type: "hstack",
        spacing: 8,
        children: [
          { type: "image", systemName: "cloud.sun.fill", color: "#ffcc00", size: 28 },
          { type: "text", content: "72°", textStyle: "largeTitle", fontWeight: "bold", color: "label" },
        ],
      },
      { type: "text", content: "Partly Cloudy", textStyle: "footnote", color: "secondaryLabel" },
      { type: "progress", value: 0.65, tint: "#4CAF50", label: "Humidity" },
    ],
  },
}, "group.com.example.myapp");

Android Glance Layout Guidelines

To reduce rendering issues on Android launchers, keep widget trees simple:

  • Prefer vstack / hstack with style props (padding, background, cornerRadius) over deep container nesting.
  • Avoid container -> vstack wrappers when a styled vstack can express the same UI.
  • In dense rows, use simple children (text, image, gauge, progress) with flex.
  • Keep hierarchy depth low (about 3-4 levels per branch in most presets).
  • Always provide progress.label so host UIs never show null.

Recommended card pattern:

{
  "type": "vstack",
  "spacing": 2,
  "padding": 8,
  "background": { "light": "#EDE9FE", "dark": "#313244" },
  "cornerRadius": 8,
  "children": [
    { "type": "text", "content": "Revenue", "textStyle": "caption2", "color": "secondaryLabel" },
    { "type": "text", "content": "$12,450", "fontSize": 18, "fontWeight": "bold", "color": "#a6e3a1" }
  ]
}

CLI — Quick Init

The plugin includes a CLI tool to scaffold native widget extensions automatically. It reads your tauri.conf.json to auto-detect the bundle identifier and app group.

macOS Widget Extension

npx tauri-plugin-widgets-api init-macos

This creates src-tauri/macos-widget/ with all required files and auto-patches your tauri.conf.json with beforeBundleCommand.

Important: Set "targets": ["app"] in bundle section of tauri.conf.json to disable Tauri's built-in DMG (it won't include the widget). The embed script rebuilds the DMG itself.

# Build + embed + DMG:
pnpm tauri build && ./src-tauri/macos-widget/embed-widget.sh

# With signing identity for production:
pnpm tauri build && ./src-tauri/macos-widget/embed-widget.sh "Developer ID Application: ..."

Options:

| Flag | Description | Default | |------|-------------|---------| | --bundle-id | Widget bundle identifier | Auto: <tauri identifier>.widgetkit | | --app-group | App Group identifier | Auto: group.<tauri identifier> | | --dir | Target directory | src-tauri/macos-widget | | --force | Overwrite existing files | false |

# Explicit identifiers:
npx tauri-plugin-widgets-api init-macos \
  --bundle-id com.example.myapp.widgetkit \
  --app-group group.com.example.myapp

iOS Widget Extension

npx tauri-plugin-widgets-api init-ios

Creates src-tauri/ios-widget/MyWidget.swift and prints step-by-step Xcode setup instructions. App Group is auto-generated from tauri.conf.json identifier (or can be overridden with --app-group). If a Widget Extension target already exists in src-tauri/gen/apple/*, the CLI also auto-syncs its generated widget Swift file.

Options:

| Flag | Description | Default | |------|-------------|---------| | --app-group | App Group identifier | Auto: group.<tauri identifier> | | --dir | Target directory | src-tauri/ios-widget | | --force | Overwrite existing files | false |

Note: Recommended flow: run init-ios once, create the Widget Extension target in Xcode, then run init-ios again to auto-replace the generated default widget code.


Environment Variables

| Variable | Scope | Default | Description | |----------|-------|---------|-------------| | WIDGET_SIGN_IDENTITY | macOS (embed-widget.sh) | ad-hoc (-) | Signing identity for widget/app re-signing. | | TAURI_WIDGET_MIN_RELOAD_SECS | iOS/macOS runtime (plugin) | Debug: 0, Release: 900 | Minimum seconds between plugin-triggered reloadAllTimelines(). Use 0 to disable plugin-side throttle. | | TAURI_DEV_HOST | Example app dev (vite.config.ts) | — | Dev host used by Tauri/Vite during tauri dev (usually set automatically). |

Examples:

# iOS dev without plugin-side reload throttle
TAURI_WIDGET_MIN_RELOAD_SECS=0 pnpm tauri ios dev

# macOS build with explicit signing identity
WIDGET_SIGN_IDENTITY="Apple Development: [email protected] (TEAMID)" \
  pnpm tauri build && ./src-tauri/macos-widget/embed-widget.sh

Platform Setup

iOS Setup

Step 1: Initialize the iOS project

pnpm tauri ios init

Step 2: Add a Widget Extension target

Open the generated Xcode project:

open src-tauri/gen/apple/*.xcodeproj

In Xcode: File → New → Target → Widget Extension. Name it (e.g. WidgetExtension), language: Swift.

In the Choose options for your new target dialog:

  • Set Product Name to WidgetExtension (or your widget name)
  • Select your Apple Team (e.g. Personal Team)
  • Keep Project as your current iOS project (e.g. myapp)
  • Set Embed in Application to your iOS app target (e.g. myapp_iOS)
  • For the basic plugin setup, disable:
    • Include Live Activity
    • Include Control
    • Include Configuration App Intent

Step 3: Add the TauriWidgets Swift Package

In Xcode:

  1. File → Add Package Dependencies...
  2. Click Add Local...
  3. Select the package folder:
    • for typical app projects: node_modules/tauri-plugin-widgets-api/swift/
    • for this repository example: swift/ (repository root)
  4. In Add to Target, choose your widget target (WidgetExtension / WidgetExtensionExtension).

Important: TauriWidgets must be linked to the widget target itself. If it is linked only to the main iOS app target, import TauriWidgets fails with no such module.

Step 4: Sync the widget code via CLI (recommended)

After creating the Widget Extension target, run:

npx tauri-plugin-widgets-api init-ios

This updates the generated src-tauri/gen/apple/*/*.swift widget entry file from the plugin template using the auto-generated App Group. It also adapts the widget struct name to match Xcode-generated *Bundle.swift references, preventing cannot find 'WidgetExtension' in scope.

Manual fallback (if you prefer to edit/copy by hand):

import SwiftUI
import WidgetKit
import TauriWidgets

struct MyWidgetEntryView: View {
    var entry: TauriWidgetEntry
    var body: some View {
        TauriWidgetView(entry: entry)
    }
}

@main
struct MyWidget: Widget {
    let kind = "ExampleWidget"
    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            // Use the same App Group that `init-ios` generated for your project.
            provider: TauriWidgetProvider(appGroup: "group.<your-tauri-identifier>")
        ) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("Powered by TauriWidgets")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Or copy from templates/ios-widget/MyWidget.swift.

Step 5: Configure App Groups

  1. Select the main app target (your iOS app, e.g. myapp_iOS) → Signing & Capabilities.
  2. Click + Capability → add App Groups.
  3. In the App Groups block click + and add your group (use the value printed by init-ios).
  4. Repeat the same for the WidgetExtension target.
  5. Verify the App Group value is exactly the same in both targets.
  6. If + Capability is disabled, set a valid Team in Signing for that target first.

Step 6: Run

pnpm tauri ios dev

macOS Setup

macOS widgets require a "sidecar" Xcode project — a small project alongside your Tauri app that compiles the Widget Extension (.appex).

Important: On macOS, WidgetKit picks up extension widgets from the installed app bundle.
Addable widgets appear only after you build the app and move the resulting .app to /Applications (or install from the DMG generated by embed-widget.sh).

Requirements: xcodegen (brew install xcodegen)

Step 1: Initialize the widget project (automatic)

# Recommended: use the CLI (auto-detects bundle ID from tauri.conf.json)
npx tauri-plugin-widgets-api init-macos

# Or with explicit identifiers:
npx tauri-plugin-widgets-api init-macos \
    --bundle-id com.example.myapp.widgetkit \
    --app-group group.com.example.myapp

This creates src-tauri/macos-widget/ with:

  • Sources/MyWidget.swift — widget code using TauriWidgetProvider
  • TauriWidgetExtension.entitlements — App Group configuration
  • project.yml — xcodegen spec
  • build-widget.sh — builds the .appex without signing (called automatically by beforeBundleCommand)
  • embed-widget.sh — injects .appex into the .app bundle, signs widget with entitlements, re-signs the app

Step 2: Configure tauri.conf.json

The CLI automatically adds beforeBundleCommand. Verify your config and make two adjustments:

{
  "build": {
    "beforeBundleCommand": "./src-tauri/macos-widget/build-widget.sh || true"
  },
  "bundle": {
    "targets": ["app"]
  }
}
  • beforeBundleCommand compiles the widget extension (without code-signing — signing is deferred to the embed step)
  • "targets": ["app"] — disable Tauri's built-in DMG creation. Tauri creates the DMG before embed-widget.sh runs, so it would not contain the widget. The embed-widget.sh script rebuilds the DMG itself after embedding the .appex.

Note: Tauri 2.x does not natively support embedding .appex via bundle.macOS.frameworks. The embed-widget.sh script handles copying the .appex into Contents/PlugIns/, signing it with its own entitlements, re-signing the app bundle with hardened runtime, and rebuilding the DMG.

Step 3: Register App Groups

Add an entitlements file to your main app with the same App Group identifier (e.g. group.com.example.myapp).

Step 4: Build & Sign

# Local development (ad-hoc signing):
pnpm tauri build && ./src-tauri/macos-widget/embed-widget.sh

# With signing identity (via env):
WIDGET_SIGN_IDENTITY="Apple Development: [email protected] (TEAMID)" \
  pnpm tauri build && ./src-tauri/macos-widget/embed-widget.sh

# Or via argument:
pnpm tauri build && ./src-tauri/macos-widget/embed-widget.sh "Apple Development: [email protected] (TEAMID)"

The full pipeline:

  1. tauri build — compiles Rust, builds .appex via beforeBundleCommand, creates .app
  2. embed-widget.sh — copies .appexContents/PlugIns/, signs with entitlements, re-signs app, rebuilds DMG, opens DMG

Tip: Add a shortcut to package.json:

{ "scripts": { "build:macos": "tauri build && ./src-tauri/macos-widget/embed-widget.sh" } }

Then just run pnpm build:macos.

Code Signing

The embed-widget.sh script signs both the widget extension (.appex) and the main app bundle. Signing identity is resolved in this order:

  1. WIDGET_SIGN_IDENTITY environment variable
  2. First argument to the script
  3. Fallback: - (ad-hoc signing)

Finding your signing identity:

security find-identity -v -p codesigning

Ad-hoc signing (-) works for local testing but has limitations:

  • Widgets appear in the gallery and render correctly
  • App Groups / UserDefaults sharing won't work — macOS requires a real Team ID for inter-process data sharing via UserDefaults(suiteName:) or containerURL(forSecurityApplicationGroupIdentifier:)

Apple Development certificate (free Apple Developer account) resolves this. The plugin uses a file-based fallback: the non-sandboxed main app writes widget_data.json directly into the widget extension's sandbox container (~/Library/Containers/<widget-bundle-id>/Data/). The widget reads it from NSHomeDirectory(). This works without provisioning profiles.

Important: The main app's App.entitlements should not include com.apple.security.app-sandbox. The Tauri app must remain non-sandboxed so it can write into the widget's container. The widget extension is always sandboxed (required by macOS for WidgetKit).

| Signing | Widget visible | Data sharing | Distribution | |---------|---------------|-------------|-------------| | Ad-hoc (-) | Yes | File-based only | Local only | | Apple Development | Yes | File-based + UserDefaults | Local + TestFlight | | Developer ID | Yes | File-based + UserDefaults | Direct distribution |

Debugging the widget separately

cd src-tauri/macos-widget
xcodegen generate
open TauriWidgetExtension.xcodeproj

Select the widget scheme in Xcode, set your app as the Host Application, and run with breakpoints.


Android Setup

Step 1: Initialize

pnpm tauri android init

The plugin automatically registers its Android bridge and Glance widget receiver.

No Kotlin code required — the built-in receiver reads widget config from shared storage and renders it via Jetpack Glance.

Step 2: Configure the App Group (optional)

By default, the plugin uses the application package name as the SharedPreferences group. To use a custom group, add a <meta-data> tag in your app's AndroidManifest.xml:

<receiver
    android:name="git.s00d.widgets.TauriGlanceWidgetReceiver"
    tools:replace="android:label"
    android:label="My Widget">
    <meta-data
        android:name="tauri_widget_group"
        android:value="group.com.example.myapp" />
</receiver>

Step 3: Run

pnpm tauri android dev

Step 4: Add widget to home screen

Long-press the home screen → Widgets → find your app → drag to screen.

Custom Widget Provider (advanced)

If you need custom behavior, create your own GlanceAppWidget / GlanceAppWidgetReceiver and keep the same shared group contract.


Desktop Setup (Windows / Linux / macOS)

Desktop widgets are transparent frameless webview windows that render the JSON config as HTML/CSS.

Option A: Declarative (tauri.conf.json)

{
  "app": {
    "windows": [
      { "label": "main", "title": "My App", "width": 800, "height": 600 },
      {
        "label": "desktop-widget",
        "url": "/widget.html",
        "width": 300, "height": 150,
        "decorations": false, "transparent": true,
        "alwaysOnBottom": true, "skipTaskbar": true,
        "visible": false, "resizable": false
      }
    ]
  }
}

Option B: Dynamic (TypeScript)

import { createWidgetWindow, closeWidgetWindow } from "tauri-plugin-widgets-api";

await createWidgetWindow({
  label: "weather",
  width: 280,
  height: 200,
  x: 80,
  y: 80,
  skipTaskbar: true,
  group: "group.com.example.myapp",
  size: "small",
});

await closeWidgetWindow("weather");

Widget Config Schema

The WidgetConfig object defines layouts per widget size family:

interface WidgetConfig {
  version?: number;
  small?: WidgetElement;
  medium?: WidgetElement;
  large?: WidgetElement;
}

Layout Containers

vstack — Vertical Stack

{
  "type": "vstack",
  "spacing": 8,
  "alignment": "leading",
  "padding": 12,
  "background": "#1a1a2e",
  "cornerRadius": 12,
  "children": [...]
}

| Property | Type | Description | |----------|------|-------------| | children | WidgetElement[] | Child elements | | spacing | number | Space between children (pt) | | alignment | "leading" \| "center" \| "trailing" | Horizontal alignment |

hstack — Horizontal Stack

{
  "type": "hstack",
  "spacing": 10,
  "alignment": "center",
  "children": [...]
}

| Property | Type | Description | |----------|------|-------------| | children | WidgetElement[] | Child elements | | spacing | number | Space between children (pt) | | alignment | "top" \| "center" \| "bottom" | Vertical alignment |

zstack — Overlay Stack

{
  "type": "zstack",
  "alignment": "center",
  "children": [...]
}

Children are layered on top of each other. Uses FrameLayout on Android.

grid — Grid Layout

{
  "type": "grid",
  "columns": 2,
  "spacing": 8,
  "rowSpacing": 8,
  "children": [...]
}

| Property | Type | Default | Description | |----------|------|---------|-------------| | columns | number | 2 | Number of columns | | spacing | number | — | Column spacing | | rowSpacing | number | — | Row spacing |

container — Single-Child Wrapper (Box)

A single-child wrapper for badges/avatars/overlays. For Android Glance stability, avoid long chains of nested container wrappers when a styled vstack is enough.

{
  "type": "container",
  "contentAlignment": "center",
  "padding": 12,
  "background": { "light": "#F0F0FF", "dark": "#1C1C1E" },
  "cornerRadius": 16,
  "clipShape": "circle",
  "children": [
    { "type": "text", "content": "AK", "fontSize": 20, "fontWeight": "bold", "color": "#ffffff" }
  ]
}

| Property | Type | Description | |----------|------|-------------| | children | WidgetElement[] | Child elements (typically one) | | contentAlignment | string | Content alignment: "center", "topLeading", "bottomTrailing", etc. |

Platform mapping: SwiftUI ZStack, Android Glance Box, HTML div with flexbox.

Leaf Elements

text

{
  "type": "text",
  "content": "Hello World",
  "fontSize": 18,
  "fontWeight": "bold",
  "fontDesign": "rounded",
  "textStyle": "headline",
  "color": "label",
  "alignment": "center",
  "lineLimit": 2
}

| Property | Type | Description | |----------|------|-------------| | content | string | Text to display | | fontSize | number | Font size in points (overridden by textStyle if set) | | fontWeight | FontWeight | ultralight thin light regular medium semibold bold heavy black | | fontDesign | FontDesign | default monospaced rounded serif | | textStyle | TextStyle | Semantic text style (see below) — respects Dynamic Type / accessibility | | color | ColorValue | Hex color, semantic name, or adaptive { light, dark } object | | alignment | TextAlignment | leading center trailing | | lineLimit | number | Max number of lines |

TextStyle values: largeTitle, title, title2, title3, headline, subheadline, body, callout, footnote, caption, caption2

When textStyle is set, it determines font size semantically based on platform settings (Dynamic Type on iOS, accessibility on Android). This ensures widgets remain readable for users with vision accessibility needs.

Android: Bold/heavy/semibold font weights are rendered via SpannableString with StyleSpan.

image

{
  "type": "image",
  "systemName": "cloud.sun.fill",
  "size": 32,
  "color": "#ffcc00"
}

| Property | Type | Description | |----------|------|-------------| | systemName | string | SF Symbol name (Apple) / drawable resource name (Android) | | data | string | Base64-encoded image data (with or without data:image/...;base64, prefix) | | url | string | Remote image URL | | size | number | Display size in points | | color | string | Tint color (hex) | | contentMode | "fit" \| "fill" | How image fills its frame |

Android: Base64 images are decoded to Bitmap and displayed in a real ImageView. System names map to drawable resources.

progress

{
  "type": "progress",
  "value": 0.7,
  "total": 1.0,
  "tint": "#4CAF50",
  "label": "Steps",
  "barStyle": "linear"
}

gauge

{
  "type": "gauge",
  "value": 0.72,
  "min": 0,
  "max": 1,
  "label": "CPU",
  "currentValueLabel": "72%",
  "tint": "#7aa2f7",
  "gaugeStyle": "circular"
}

chart

{
  "type": "chart",
  "chartType": "bar",
  "tint": "#89b4fa",
  "chartData": [
    { "label": "Mon", "value": 120 },
    { "label": "Tue", "value": 180, "color": "#a6e3a1" }
  ]
}

Supported types: bar, line, area, pie.

list — Collection List

{
  "type": "list",
  "spacing": 4,
  "items": [
    { "text": "Buy groceries", "checked": true, "action": "task_buy" },
    { "text": "Call dentist", "checked": false, "action": "task_call", "payload": "id=42" }
  ]
}

| Property | Type | Description | |----------|------|-------------| | items | ListItem[] | List rows with text and optional checked/action/payload | | spacing | number | Space between rows |

ListItem:

| Field | Type | Description | |-------|------|-------------| | text | string | Row label | | checked | boolean | Optional checked marker | | action | string | Optional action emitted as widget-action | | payload | string | Optional payload for widget-action |

Platform notes:

  • Android: rendered via Glance list/lazy composition.
  • iOS/macOS: rendered as SwiftUI row list.
  • Desktop: rendered as HTML row list.

button

{
  "type": "button",
  "label": "Refresh",
  "action": "refresh_data",
  "backgroundColor": "#4CAF50",
  "color": "#ffffff"
}

| Property | Type | Description | |----------|------|-------------| | label | string | Button text | | url | string | Deep-link URL | | action | string | Action identifier — emits widget-action event | | backgroundColor | string | Background color (hex) | | color | string | Text color (hex) |

Listen for actions:

import { onWidgetAction } from "tauri-plugin-widgets-api";

await onWidgetAction((data) => {
  console.log("Action:", data.action, data.payload);
});

toggle

{ "type": "toggle", "isOn": true, "label": "Dark Mode", "tint": "#4CAF50" }

divider

{ "type": "divider", "color": "#333333", "thickness": 1 }

spacer

{ "type": "spacer", "minLength": 10 }

date

{ "type": "date", "date": "2026-03-01T10:00:00Z", "dateStyle": "relative", "fontSize": 14, "color": "#ffffff" }

| dateStyle | Output Example | |-------------|----------------| | time | 10:00 AM | | date | Mar 1, 2026 | | relative | in 2 days | | offset | +2 days | | timer | 48:00:00 |

link — Tappable Wrapper

Wraps any child content and makes it tappable:

{
  "type": "link",
  "action": "open_profile",
  "children": [
    { "type": "text", "content": "Tap me!", "fontSize": 16, "color": "#60a5fa" }
  ]
}

shape

{ "type": "shape", "shapeType": "circle", "fill": "#ef4444", "size": 12 }

Supported shapes: circle, capsule, rectangle.

timer

{
  "type": "timer",
  "targetDate": "2026-12-31T23:59:59Z",
  "counting": "down",
  "fontSize": 24,
  "fontWeight": "bold",
  "color": "#ffffff"
}

iOS/macOS: Uses Text(date, style: .timer) which updates natively every second without consuming widget refresh budget.

label — Icon + Text

{
  "type": "label",
  "text": "5 new messages",
  "systemName": "envelope.fill",
  "iconColor": "#60a5fa",
  "fontSize": 14,
  "color": "#e2e8f0"
}

canvas — Declarative Drawing

{
  "type": "canvas",
  "width": 120,
  "height": 120,
  "elements": [
    { "draw": "circle", "cx": 60, "cy": 60, "r": 55, "fill": "#1e293b", "stroke": "#475569", "strokeWidth": 2 },
    { "draw": "line", "x1": 60, "y1": 60, "x2": 60, "y2": 20, "stroke": "#f1f5f9", "strokeWidth": 3, "lineCap": "round" },
    { "draw": "circle", "cx": 60, "cy": 60, "r": 4, "fill": "#ef4444" }
  ]
}

Draw commands: circle, line, rect, arc, text, path.

Android: Canvas bitmaps are capped at 512px and compressed to stay under the 500KB Binder IPC limit, preventing TransactionTooLargeException.

Common Style Properties

All elements support:

{
  "padding": 12,
  "background": "#1a1a2e",
  "cornerRadius": 8,
  "opacity": 0.9,
  "frame": { "width": 100, "height": 50, "maxWidth": "infinity" },
  "border": { "color": "#333333", "width": 1 },
  "shadow": { "color": "#000000", "radius": 4, "x": 0, "y": 2 },
  "clipShape": "circle",
  "flex": 1
}

clipShape — Content Masking

Clips the element's content to a shape. Useful for circular avatars, pill-shaped badges, etc.

| Value | Description | |-------|-------------| | "circle" | Perfect circle (50% border-radius) | | "capsule" | Pill shape (fully rounded ends) | | "rectangle" | Rectangle with cornerRadius applied |

Platform mapping: SwiftUI .clipShape(), CSS border-radius + overflow: hidden, Android Glance shape clipping.

flex — Proportional Layout

The flex property allows elements to occupy available space proportionally within stacks, analogous to CSS flex, Android layout_weight, or SwiftUI layoutPriority.

{
  "type": "hstack",
  "children": [
    { "type": "text", "content": "Left", "flex": 1 },
    { "type": "text", "content": "Center (wider)", "flex": 2 },
    { "type": "text", "content": "Right", "flex": 1 }
  ]
}

Adaptive Colors (Dark Mode Support)

All color properties (color, tint, fill, stroke, backgroundColor, iconColor, background) support three formats:

1. Hex string (static):

{ "color": "#FF5733" }

2. Semantic color name (auto-adapts to system theme):

{ "color": "label" }

Available semantic colors: label, secondaryLabel, tertiaryLabel, systemBackground, secondarySystemBackground, separator, accent, systemRed, systemGreen, systemBlue, systemOrange, systemYellow, systemPurple, systemPink, systemGray

3. Adaptive object (explicit light/dark values):

{ "color": { "light": "#000000", "dark": "#FFFFFF" } }

The background property additionally supports gradients:

{
  "background": { "light": "#FFFFFF", "dark": "#1C1C1E" }
}

Gradient Backgrounds

{
  "background": {
    "gradientType": "linear",
    "colors": ["#667eea", "#764ba2"],
    "direction": "topToBottom"
  }
}

Types: linear, radial, angular.


Widget Updater

For periodic updates (clocks, dashboards):

import { startWidgetUpdater } from "tauri-plugin-widgets-api";

const stop = await startWidgetUpdater(
  () => ({
    small: {
      type: "vstack", padding: 12, background: "#1a1a2e",
      children: [
        { type: "text", content: new Date().toLocaleTimeString(),
          fontSize: 32, fontWeight: "bold", color: "#fff" },
      ],
    },
  }),
  "group.com.example.myapp",
  {
    intervalMs: 60_000,
    reload: true,
    onAction: (action, payload) => {
      console.log("Widget action:", action, payload);
    },
  },
);

// Later:
stop();

| Option | Type | Default | Description | |--------|------|---------|-------------| | intervalMs | number | 1000 | Update interval (ms). Use 60000+ for native widgets. | | immediate | boolean | true | Run builder immediately on start | | reload | boolean | false | Call reloadAllTimelines() after each tick. Throttled by TAURI_WIDGET_MIN_RELOAD_SECS on iOS/macOS. | | onAction | function | — | Subscribe to widget-action events |

Important: Apple enforces a daily widget reload budget (~40-70 reloads/day). The Rust backend throttle is configurable via TAURI_WIDGET_MIN_RELOAD_SECS.

Defaults:

  • Debug: 0 (no plugin-side throttle)
  • Release: 900 (15 minutes)

Examples:

  • Disable plugin-side throttle: TAURI_WIDGET_MIN_RELOAD_SECS=0
  • Set custom throttle: TAURI_WIDGET_MIN_RELOAD_SECS=5

Even with 0, WidgetKit may still coalesce/defer refreshes — this is a platform-level limit. For second-by-second UI, prefer native timer/date styles (for example, { type: "timer" }) instead of frequent reload calls.


Low-Level Data API

import { setItems, getItems, reloadAllTimelines } from "tauri-plugin-widgets-api";

await setItems("temperature", "72", "group.com.example.myapp");
const temp = await getItems("temperature", "group.com.example.myapp");
await reloadAllTimelines();

API Reference

| Function | Description | |----------|-------------| | setItems(key, value, group) | Store a key-value pair | | getItems(key, group) | Read a stored value | | setWidgetConfig(config, group, skipReload?) | Send a full UI config | | getWidgetConfig(group) | Read the current UI config | | setRegisterWidget(widgets) | Register widget provider class names | | reloadAllTimelines() | Reload all widget timelines | | reloadTimelines(ofKind) | Reload a specific widget kind | | requestWidget() | Pin a widget (Android only) | | createWidgetWindow(config) | Create a desktop widget window | | closeWidgetWindow(label) | Close a desktop widget window | | widgetAction(action, payload?) | Emit a widget-action event | | onWidgetAction(callback) | Listen for widget-action events | | startWidgetUpdater(builder, group, options?) | Periodic config updater |


Rust API

use tauri::Manager;
use tauri_plugin_widgets::WidgetExt;

fn update_widget(app: &tauri::AppHandle) {
    let widget = app.widget();
    widget.set_items("key", "value", "group").unwrap();
    widget.reload_all_timelines().unwrap();
}

Project Structure

├── android/                    Android plugin (Jetpack Glance renderer)
│   └── src/main/java/
│       ├── TauriGlanceWidget.kt       Glance UI renderer
│       ├── WidgetBridgePlugin.kt       Tauri bridge
│       ├── TauriGlanceWidgetReceiver.kt Universal AppWidget receiver
│       └── WidgetActionReceiver.kt    Button action handler
├── ios/                        iOS plugin (Tauri bridge)
│   └── Sources/
│       └── WidgetPlugin.swift         setItems/getItems/reload via FileManager
├── macos/
│   └── WidgetReload.swift      Minimal FFI bridge (reload + shared container path)
├── swift/                      TauriWidgets Swift Package
│   ├── Package.swift
│   └── Sources/TauriWidgets/
│       ├── Models.swift                Widget UI models (public, Codable)
│       ├── DynamicElementView.swift    SwiftUI renderer
│       ├── TauriWidgetProvider.swift   TimelineProvider + TauriWidgetView
│       ├── WidgetDataStore.swift       App Group file I/O
│       └── WidgetActionIntent.swift    AppIntent for button actions
├── src/                        Rust plugin core
│   ├── lib.rs                  Plugin init + commands
│   ├── desktop.rs              Desktop: file storage + widget windows
│   ├── mobile.rs               Mobile: native bridge + throttled reload
│   └── models.rs               WidgetConfig / WidgetElement models
├── guest-js/                   TypeScript API
│   └── index.ts                All exports + startWidgetUpdater
├── templates/                  Starter files for widget extensions
│   ├── ios-widget/MyWidget.swift
│   └── macos-widget/
│       ├── MyWidget.swift              Widget Swift code template
│       ├── Entitlements.plist          App Group entitlements
│       ├── project.yml                 xcodegen project spec
│       └── build-widget.sh            Build script (xcodebuild)
├── bin/
│   └── cli.mjs                        CLI tool (npx tauri-plugin-widgets-api)
├── widget.html                 Desktop widget HTML renderer
├── build.rs                    Minimal: tauri_plugin::Builder + macOS FFI bridge
├── GUIDE.md                    Русскоязычная инструкция запуска примера
└── examples/
    └── tauri-plugin-widgets-example/   Complete demo app

Troubleshooting

Widget shows "No configuration" (macOS)

  • Call setWidgetConfig(...) from your app before adding the widget.
  • Verify the app is signed with a real certificate (security find-identity -v -p codesigning), not ad-hoc.
  • Check that the app is not sandboxed (App.entitlements should not contain com.apple.security.app-sandbox).
  • Verify the widget's container has the data file:
    ls ~/Library/Containers/<your-bundle-id>.widgetkit/Data/widget_data.json
  • Check widget logs: log show --last 1m --predicate 'subsystem == "com.tauri.widgets"' --style compact
  • Ensure TauriWidgetExtension.entitlements contains com.apple.security.app-sandbox and the correct App Group.

Widget shows "No configuration" (iOS)

  • Ensure the App Group identifier is identical in the main app and widget extension (Xcode → Signing & Capabilities → App Groups).
  • Call setWidgetConfig(...) from your app before adding the widget.

Widget doesn't update (iOS/macOS)

  • Apple limits widget refreshes to ~40-70 per day.
  • Check plugin throttle: TAURI_WIDGET_MIN_RELOAD_SECS (Debug default 0, Release default 900).
  • For testing: in Xcode, use Debug → Simulate Timeline → After Refresh.
  • For live counters, use { type: "timer" } instead of frequent reloads.

Android widget is empty or crashes

  • Check adb logcat | grep -i widget for errors.
  • Canvas bitmaps larger than 512px are automatically capped.
  • Bitmap data is compressed to stay under the Binder IPC 500KB limit.

Desktop widget window not appearing

  • Ensure createWidgetWindow(...) is called, or the window is defined in tauri.conf.json.
  • Add "desktop-widget" to the windows array in capabilities/default.json.

License

MIT