Practical Examples

Real-world notification patterns for Tauri apps using React, covering task alerts, download completion, scheduled reminders, and interactive reply actions.

When you build a Tauri app, notifications bridge the gap between background events and a user’s attention. The previous sections covered how to request permission and send a basic notification. This page turns those building blocks into real patterns you will use in a production application — a task marked complete, a file that finished downloading, a reminder that fires after you close the app, and an interactive message that lets the user reply directly from the notification shade. Each example is a fully runnable React component that works with @tauri-apps/plugin-notification.

Prerequisites:

These examples assume you already installed the notification plugin and registered it in your Tauri app as described in the Introduction. If the plugin is not registered, the functions will throw a runtime error.

Task completion alert

Notifying a user that a long-running operation finished is one of the simplest and most useful patterns. A task — an export, a data sync, a computation — completes in the background and the user sees a toast even if the window is minimised.

The React component below requests permission on first use (the “Enable Notifications” button triggers the native prompt on your platform) and then sends a clean notification when the task is done.

src/components/TaskComplete.tsx
import { useState } from "react";
import {
  isPermissionGranted,
  requestPermission,
  sendNotification,
} from "@tauri-apps/plugin-notification";
function TaskComplete() {
  const [permissionGranted, setPermissionGranted] = useState(false);
  const enableNotifications = async () => {
    let granted = await isPermissionGranted();
    if (!granted) {
      const permission = await requestPermission();
      granted = permission === "granted";
    }
    setPermissionGranted(granted);
  };
  const handleTaskComplete = async () => {
    if (!permissionGranted) return;
    sendNotification({
      title: "Task Completed",
      body: "The data export has finished successfully.",
    });
  };
  return (
    <div>
      {!permissionGranted && (
        <button onClick={enableNotifications}>Enable Notifications</button>
      )}
      <button onClick={handleTaskComplete} disabled={!permissionGranted}>
        Complete Task
      </button>
    </div>
  );
}
export default TaskComplete;

The enableNotifications function checks the existing state first so the user does not see the native dialog again if they already granted permission. Once the state flips to true, the “Complete Task” button becomes active and fires sendNotification. Notice that calling requestPermission without a user gesture is allowed in Tauri — the plugin triggers a native OS dialog, not a browser prompt — but keeping the request behind a button click respects user expectations.

Windows development behaviour:

On Windows, notifications may not appear during development because the OS only displays notifications for installed applications. The PowerShell window name and icon might also be used instead of your app’s identity. Test on an installed build to see the real notification.

Download finished notification

A file download is a natural fit for a notification. You can include an icon, a custom sound, and a summary that the user can expand on platforms that support it. This example simulates a download with a short delay and then sends a styled notification.

src/components/DownloadNotice.tsx
import { useState } from "react";
import {
  isPermissionGranted,
  requestPermission,
  sendNotification,
} from "@tauri-apps/plugin-notification";
function DownloadNotice() {
  const [permissionGranted, setPermissionGranted] = useState(false);
  const [downloading, setDownloading] = useState(false);
  const enableNotifications = async () => {
    let granted = await isPermissionGranted();
    if (!granted) {
      const permission = await requestPermission();
      granted = permission === "granted";
    }
    setPermissionGranted(granted);
  };
  const handleDownload = async () => {
    if (!permissionGranted || downloading) return;
    setDownloading(true);
    // Simulate a network download
    await new Promise((resolve) => setTimeout(resolve, 3000));
    sendNotification({
      title: "Download Finished",
      body: "report-q4.pdf is ready",
      icon: "asset:///file_download_icon.png",
      sound: "notification.wav",
      autoCancel: true,
    });
    setDownloading(false);
  };
  return (
    <div>
      {!permissionGranted && (
        <button onClick={enableNotifications}>Enable Notifications</button>
      )}
      <button
        onClick={handleDownload}
        disabled={!permissionGranted || downloading}
      >
        {downloading ? "Downloading…" : "Download File"}
      </button>
    </div>
  );
}
export default DownloadNotice;

The autoCancel: true option tells the OS to dismiss the notification after the user interacts with it or after a short time — the default behaviour on most platforms, but making it explicit avoids stale notifications piling up. The icon points to an asset bundled with the application using the asset:// protocol. Place a PNG with that filename in the Tauri resource directory (configured in tauri.conf.json) or in the icons folder so it is available at build time.

Missing asset path causes silent failure:

If the icon URL does not resolve to a valid bundled asset, the notification may still appear but without the image. On some platforms a broken icon reference can prevent the notification from showing at all. Always test asset paths on every target OS.

Scheduled reminder

Notifications that fire at a specific future time — even when the app is closed — are possible with the schedule option. This example sets a reminder 15 seconds in the future, and also shows how to cancel it before it fires.

src/components/Reminder.tsx
import { useState } from "react";
import {
  isPermissionGranted,
  requestPermission,
  sendNotification,
  cancel,
  Schedule,
} from "@tauri-apps/plugin-notification";
function Reminder() {
  const [permissionGranted, setPermissionGranted] = useState(false);
  const [scheduledId, setScheduledId] = useState<number | null>(null);
  const enableNotifications = async () => {
    let granted = await isPermissionGranted();
    if (!granted) {
      const permission = await requestPermission();
      granted = permission === "granted";
    }
    setPermissionGranted(granted);
  };
  const setReminder = async () => {
    if (!permissionGranted) return;
    const id = Date.now();
    setScheduledId(id);
    sendNotification({
      id,
      title: "Reminder",
      body: "Time to stretch your legs!",
      schedule: Schedule.at(new Date(Date.now() + 15_000)),
    });
  };
  const cancelReminder = async () => {
    if (scheduledId === null) return;
    await cancel(scheduledId);
    setScheduledId(null);
  };
  return (
    <div>
      {!permissionGranted && (
        <button onClick={enableNotifications}>Enable Notifications</button>
      )}
      <button
        onClick={setReminder}
        disabled={!permissionGranted || scheduledId !== null}
      >
        Set Reminder
      </button>
      {scheduledId !== null && (
        <button onClick={cancelReminder}>Cancel Reminder</button>
      )}
    </div>
  );
}
export default Reminder;

Schedule.at accepts a JavaScript Date object. A second boolean parameter turns the notification into a repeating alarm — useful for daily reminders — but repeated schedules need careful handling on mobile because the OS may batch or defer them if the device is in low-power mode.

Platform scheduling limits:

On Android, scheduled notifications use the system alarm service and will fire even if the app is not running. On iOS, scheduling works through the User Notifications framework but may be subject to system throttling. Desktop platforms have varying support; always test on real devices.

Calling cancel with the notification’s id removes it from the pending queue. If you do not store the id, you cannot cancel it later, so always keep a reference in component state or a global store when building an app that lets users manage reminders.

Interactive notification with a reply action

Some notifications deserve more than a dismiss. An incoming message notification can include a “Reply” button that opens a text input right inside the notification shade. The plugin supports action types that you register once, then reference when sending a notification. Listening to the onAction event lets you react to the user’s choice.

First, the React component registers an action type for messages, listens for action events, and sends a notification that uses the registered action.

src/components/MessageAlert.tsx
import { useState, useEffect } from "react";
import {
  isPermissionGranted,
  requestPermission,
  registerActionTypes,
  onAction,
  sendNotification,
} from "@tauri-apps/plugin-notification";
import type { ActionPerformed } from "@tauri-apps/plugin-notification";
function MessageAlert() {
  const [permissionGranted, setPermissionGranted] = useState(false);
  useEffect(() => {
    registerActionTypes([
      {
        id: "messages",
        actions: [
          {
            id: "reply",
            title: "Reply",
            input: true,
            inputButtonTitle: "Send",
            inputPlaceholder: "Type your reply...",
          },
          {
            id: "mark-read",
            title: "Mark as Read",
            foreground: false,
          },
        ],
      },
    ]);
    const unlisten = onAction((notification: ActionPerformed) => {
      if (notification.actionId === "reply" && notification.input) {
        console.log("User replied:", notification.input);
      } else if (notification.actionId === "mark-read") {
        console.log("Marked as read");
      }
    });
    return () => {
      unlisten.then((fn) => fn());
    };
  }, []);
  const enableNotifications = async () => {
    let granted = await isPermissionGranted();
    if (!granted) {
      const permission = await requestPermission();
      granted = permission === "granted";
    }
    setPermissionGranted(granted);
  };
  const sendMessageNotification = () => {
    if (!permissionGranted) return;
    sendNotification({
      title: "New Message",
      body: "Alice: Are you free for lunch?",
      actionTypeId: "messages",
    });
  };
  return (
    <div>
      {!permissionGranted && (
        <button onClick={enableNotifications}>Enable Notifications</button>
      )}
      <button onClick={sendMessageNotification} disabled={!permissionGranted}>
        Simulate Incoming Message
      </button>
    </div>
  );
}
export default MessageAlert;

Registering action types is a one-time setup — the useEffect with an empty dependency array runs once when the component mounts. The onAction listener receives an ActionPerformed object that contains the action id, the notification id, and any text input the user typed. Returning the cleanup function from useEffect ensures the listener is removed when the component unmounts.

Android channels and actions:

On Android, actionable notifications must be sent to a channel with an importance level of High or above. Create the channel before sending the notification:

import { createChannel, Importance } from '@tauri-apps/plugin-notification';
await createChannel({
  id: 'messages',
  name: 'Messages',
  importance: Importance.High,
});

The channel id must match the actionTypeId used when registering actions and sending the notification.

The foreground: false property on the “Mark as Read” action keeps the app in the background when the user taps it — useful for quick, non-disruptive actions. The “Reply” action sets input: true, which opens a text field directly inside the notification.

What to watch for when combining patterns

You now have isolated components for each scenario. In a real codebase you would likely centralise the permission logic into a custom hook or a context provider so every feature that sends notifications does not duplicate the permission check. That avoids race conditions where two components both try to call requestPermission simultaneously — which can cause undefined behaviour across platforms.

When scheduling reminders while also using interactive notifications, keep the id space distinct. If a scheduled reminder accidentally shares an id with an active actionable notification, cancelling one might remove the other. Prefix ids with a category string: reminder-${timestamp} and msg-${msgId}.

Confirming everything works:

After implementing any example, build the app for your target platform and trigger the notification. If the OS notification appears with the correct title and body, your plugin setup and permissions are correct. If nothing appears, double‑check the capability file includes "notification:default" and that the plugin is registered in lib.rs.

What this unlocks

Each example isolates one interaction pattern, but together they form the foundation of a notification‑aware Tauri application. A task runner, a download manager, a calendar app, or a messaging client all map directly to the components shown here. The notification plugin is the surface; the real power comes from wiring it to the rest of your app’s logic.