Repository navigation
api toast
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.
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 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 overicon. -
duration(number, optional): seconds before auto-dismiss;0= until closed (default: 8). A0-duration toast is persistent: it does not count toward the stack cap and is never evicted — only the × button, a matchingstackKeyreplacement, or programmatic removal ends it. -
color(string, optional): accent color as strict hex (#rgbor#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. AbackgroundImagecovers 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 noonClick— clicking anywhere dismisses (that counts as a dismissal —onDismissfires). -
animation(string, optional): content animation, billboards only — ignored without asize, 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 onprefers-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 bodyonClickhandles it, with the same dismissal semantics. Shown only on'small'|'medium'|'large'billboards and only whenonClickis a function — a call to action without an action would be a lie. Because Blacksmith's cross-client relays strip callbacks, only receipt-sideshow()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 withimage(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 byclearByModule(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.onDismissdoes not fire afteronClick. Runs in Blacksmith's context — keep it self-contained, same rule as menubar toolonClick. -
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 samestackKeyreplaces 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/streamcapture page, typically recorded by OBS), or'both'. Anything else falls back to'game'. Checked receipt-side againstgame.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 ontoastExcludedUsers, overriding bothchanneland the persistent-toast rule. For deliberate, human-directed sends only — a GM explicitly choosing an excluded recipient. Automated senders usechannel. -
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 intoastExcludedUsersstill 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 withregisterChannel()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 a toast programmatically. Silent — onDismiss does not fire. Returns boolean.
Remove all toasts owned by a module (e.g. on your module's cleanup). Silent. Returns the count removed.
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.
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.
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.
- Ai assistance
- Applicationv2 traps
- CSS traps
- dnd5e conditions
- Documentation standard
- Token actor identity
- Campaign
- Canvas
- Chatcards
- Compendiums
- Contextmenu
- Core
- Create journal entry
- Dialog
- Active Effects
- Entity list
- Equiplocations
- Geography
- GM request
- Gmnotes
- Health
- Hookmanager
- Importer
- Inventory
- Menubar
- Notes
- Party
- Pins
- Quantity split
- Requestroll
- Rolls
- Scene config
- Sockets
- Stats
- Tags
- Toast
- Tokens
- Toolbar
- Window
- Worldclock