Skip to content

api toast

github-actions[bot] edited this page Aug 31, 2026 · 17 revisions

Toast API

Audience: module authors showing a short on-screen message.

On-screen transient toasts, exposed as module.api.toast. A toast pops up over the play area (top-center stack), shows a title/subtitle with an icon or image, and auto-dismisses — optionally clickable to run a consumer action.

This is a local, per-client primitive. show() renders on the client that calls it, nothing more. There is no cross-client delivery in this API: a consumer whose event already reached this client (its own socket message, a world-setting sync, a document hook) calls show() receipt-side. Because toasts never cross the socket, onClick/onDismiss are plain function references.

This is deliberately not Foundry's ui.notifications: that is a core-styled top-center text queue — not actionable, not themeable, and it has no image slot.

Source: scripts/api-toast.js. Styles: styles/toast.css.

Accessing

const toast = game.modules.get('coffee-pub-blacksmith')?.api?.toast;

Available from init (assigned in the early synchronous API block — see the bootstrap notes in architecture-blacksmith.md §3).

show(config)

Show a toast on this client. Returns a toast ID (string), or null on error.

If the current user's name appears in the Excluded Users world setting (toastExcludedUsers, Notifications settings — a comma-separated, case-insensitive list of Foundry user names), show() renders nothing and returns null on the tabletop (/game). The check runs on the receiving client, so it suppresses every delivery path — direct consumer calls and Blacksmith's internal cross-client relays alike. It exists for accounts that cannot interact with the screen, e.g. a camera or stream login. The /stream view is exempt: a stream-targeted toast (see publish) renders there even when the logged-in account is on the exclusion list — exclusion protects a passive account from tabletop noise, while publishing to the stream is deliberate.

Exclusion is not all-or-nothing. A toast declaring a channel still renders for an excluded user unless the GM has switched that channel off. Every channel is on by default, so the feature works with no configuration. A toast with no channel is never affected, and the check is receipt-side, so this changes what a client renders and never what was delivered.

Declare your channels so each one gets a labelled checkbox in the Notifications settings:

api.toast.registerChannel('crit', {
    moduleId: 'coffee-pub-bibliosoph',
    label: 'Critical Hits',
    description: 'Natural 20 announcements'
});

Each registered channel becomes an ordinary Boolean world setting, defaulting to on, titled with the label and description you supply. Register at init.

Registration is optional. An unregistered channel behaves identically and is likewise allowed — it simply has no checkbox, so a GM cannot switch it off and will not know it exists. Declaring is how you give them the choice.

api.toast.getChannels() returns what is registered right now: read it as "what is loaded", never as "what is valid". A module absent for one session takes its channels out of that list while the GM's stored preference for them survives untouched, so never use it to prune anything.

Channel names belong to the sending module: Blacksmith matches the string and nothing more, so it never has to know what a critical or an injury is. Pick a name for a class of event and document it wherever your module documents its behaviour, so a GM knows what to allow through.

Prefer a name per class of event the GM might plausibly want to separate, and one shared name where they would not. A GM may well want dice moments on camera and injuries off it, so those earn distinct channels; splitting four variants of the same table gag into four names only adds chances to mistype a distinction nobody draws.

A channel typed into the setting that nothing sends does nothing, silently, and Blacksmith cannot tell a typo from another module's channel. Two things help: every channel name is logged the first time it is seen on a client (debug mode), which gives a GM the list actually in use; and a module can check delivery itself with the introspection helpers below.

A persistent toast (duration: 0) never bypasses exclusion, whatever its channel. Exclusion exists because nobody is behind that screen to dismiss anything, so a persistent toast let through would sit on a capture surface for the rest of the session. Bypass channels are for transient moments. The /stream view is unaffected, being exempt from exclusion entirely.

bypassExclusion: true overrides all of it, including the persistent rule. It exists for a human deciding one send at a time — a GM ticking an excluded recipient in the Send Toast window — and automated senders must not set it. Use channel instead, which leaves the decision with the GM in settings rather than in your code.

Channels Blacksmith itself sends. Blacksmith names channels for its own toasts like any other sender:

Channel Sent by
timer Session, break, and turn timer announcements

show() also renders only on the view its publish config targets: by default toasts appear on the active tabletop (/game) and never on Foundry's chat-only /stream capture view — on a non-targeted view show() renders nothing and returns null. See the publish config below to target the stream view or both.

Config:

  • title (string, required): headline text
  • subtitle (string, optional): second line
  • icon (string, optional): FontAwesome icon class
  • image (string, optional): image path/URL, rendered as a round avatar. Wins over icon.
  • duration (number, optional): seconds before auto-dismiss; 0 = until closed (default: 8). A 0-duration toast is persistent: it does not count toward the stack cap and is never evicted — only the × button, a matching stackKey replacement, or programmatic removal ends it.
  • color (string, optional): accent color as strict hex (#rgb or #rrggbb; anything else renders the default look). Drives the border, icon, and title. Validated and applied as a CSS custom property — arbitrary CSS cannot be injected.
  • backgroundColor (string, optional): box background color as strict hex, independent of the accent (default: the dark base). Rendered slightly translucent (alpha 0.9) so the play area reads through, matching the default look. A backgroundImage covers it when both are set.
  • size (string, optional): omit for a normal toast (content-fit, stacks top-center). 'small' | 'medium' | 'large' | 'fullscreen' render a billboard instead: a viewport-proportional box (widths roughly 26 / 40 / 58 percent of the viewport, fullscreen 100×100 with a dark scrim; heights are per-preset minimums that grow with the content, so long messages enlarge the box rather than scrolling), centered on screen, with typography scaling relationally with the box. Billboards are singletons (a new one replaces the current, whatever its size), exempt from the stack cap, and — with no onClick — clicking anywhere dismisses (that counts as a dismissal — onDismiss fires).
  • animation (string, optional): content animation, billboards only — ignored without a size, so stacked toasts always render still. 'pop' scales the content in with a springy bounce; 'reveal' stages the entrance (icon, then title, then subtitle); 'pulse' is a subtle infinite breathe meant for persistent (duration: 0) billboards; 'slam' smashes in from oversized with a jolt on impact; 'shake' rattles in with a decaying wobble. Anything else renders without animation. Pure CSS on the content children, Not gated on prefers-reduced-motion: a billboard's entrance is the announcement, and that query follows an OEM-settable toggle on Windows, so gating it silenced the effect for people who had chosen nothing.
  • callToAction (string, optional): a button-styled label (e.g. "Roll for the Crit Card") rendered below the text, making it visually unmistakable that the toast wants a click. It is not a separate control — there is still exactly one click action: the CTA sits inside the toast's single click target and the body onClick handles it, with the same dismissal semantics. Shown only on 'small' | 'medium' | 'large' billboards and only when onClick is a function — a call to action without an action would be a lie. Because Blacksmith's cross-client relays strip callbacks, only receipt-side show() calls (the intended local-first consumer pattern) can display one. Styled from the accent color; ignored in every other case.
  • backgroundImage (string, optional): image path/URL rendered as a cover background behind the toast content, with an automatic dark scrim so text stays legible. Combines with image (the avatar floats over it); best with the larger sizes and fullscreen.
  • sound (string, optional): audio path played locally when the toast appears. Cross-client Blacksmith relays carry the path as data and each recipient plays it locally.
  • moduleId (string, optional): owning module, used by clearByModule (default: "blacksmith-core")
  • onClick (Function, optional): makes the toast clickable (pointer cursor, hover affordance, button sound on click). Called with the click event when the user clicks the toast body; the toast is then removed. onDismiss does not fire after onClick. Runs in Blacksmith's context — keep it self-contained, same rule as menubar tool onClick.
  • onDismiss (Function, optional): fires only when the toast goes away without being acted on — see the dismiss semantics below.
  • stackKey (string, optional): toasts stack by default (capped; oldest evicted). A new toast with the same stackKey replaces the existing one in place — use for "latest state wins" toasts (unread counters, current leader) instead of stacking duplicates.
  • publish (string, optional): which Foundry view renders the toast — 'game' (the active tabletop, default), 'stream' (the chat-only /stream capture page, typically recorded by OBS), or 'both'. Anything else falls back to 'game'. Checked receipt-side against game.view, so it holds across every delivery path; plain data, so it rides Blacksmith's cross-client relays unchanged.
  • bypassExclusion (boolean, optional): render even for a user on toastExcludedUsers, overriding both channel and the persistent-toast rule. For deliberate, human-directed sends only — a GM explicitly choosing an excluded recipient. Automated senders use channel.
  • channel (string, optional): free-form name grouping this toast with others of its kind, e.g. 'crit'. Its only effect is on exclusion — a user listed in toastExcludedUsers still sees a channelled toast unless the GM has switched that channel off, and every channel is on by default. Omit it and the toast never reaches an excluded user. Plain data, so it rides the cross-client relays unchanged. Pair it with registerChannel() to give the GM a checkbox.

Title and subtitle are rendered as text, never parsed as HTML.

Dismiss semantics — the same contract as menubar notifications (api-menubar.md, addNotification), extended with the toast-only paths:

Removal path onDismiss fires?
Auto-timeout (duration elapses) Yes
User clicks the × close button Yes
User clicks the body (onClick ran) No — the click already told you
Consumer calls remove(id) No — you initiated it
clearByModule(moduleId) No — bulk teardown
Replaced via stackKey No — superseded, not dismissed
Evicted by the stack cap No

Examples:

const toast = game.modules.get('coffee-pub-blacksmith').api.toast;

// Passive toast
toast.show({
    title: "Quest complete",
    subtitle: "The Ruined Tower",
    icon: "fa-solid fa-trophy",
    moduleId: "my-module"
});

// Actionable message splash with an avatar — click opens the conversation
toast.show({
    title: `Message from ${senderUser.name}`,
    subtitle: "Click to open the conversation",
    image: senderUser.avatar,
    duration: 8,
    moduleId: "my-module",
    stackKey: "my-module-incoming-message",   // a newer message replaces this splash
    onClick: () => openMessagesWindow({ conversationId }),
    onDismiss: () => { /* went away unread */ }
});

remove(toastId)

Remove a toast programmatically. Silent — onDismiss does not fire. Returns boolean.

clearByModule(moduleId)

Remove all toasts owned by a module (e.g. on your module's cleanup). Silent. Returns the count removed.

getActive()

Returns Array<{ id, moduleId, stackKey, persistent, shownAt, color, backgroundColor, size, animation, callToAction }> for the toasts currently on screen — display metadata only, no elements or callbacks. shownAt is Date.now() at render, so Date.now() - shownAt is the age in milliseconds; persistent is true for duration: 0, the only toasts that will not clear themselves.

isExcludedUser(user) and isBypassChannel(channel)

Read-only introspection, so a module can diagnose its own delivery rather than re-deriving it. isExcludedUser(user) (default: the current user) reports whether a user is listed in toastExcludedUsers; isBypassChannel(channel) reports whether that channel currently reaches excluded users. Both read world settings, so any client can call them.

Use them to warn a GM that a module's announcements cannot arrive — a user excluded with every one of the module's channels switched off. Ask rather than reading the settings directly: the setting ids and the key naming are implementation, and these two answers are not. That coupling is not hypothetical — the storage behind channels changed from a single comma-separated field to one setting per channel, and callers of these helpers needed no change.

Stacking model

Toasts stack vertically at top-center, newest at the bottom of the stack. The cap (see MAX_STACK in scripts/api-toast.js) applies to transient toasts only: when full, the oldest transient toast is evicted silently. Persistent (duration: 0) toasts sit outside the cap and are never evicted. Same-stackKey toasts replace in place regardless of persistence. Rendering is DOM-direct — there is no template or re-render cycle; see architecture-toast.md for the mechanism.

Clone this wiki locally