On this pageOverview
Toast
A stack of transient notifications anchored to a corner of the viewport. Each entry has its own enter and leave animation, its own auto-dismiss timer, and its own hover-to-pause behavior. One container lives at the app root; entries are added dynamically via Toast.show.
Toast is parameterized on a user-provided payload schema. The component owns only lifecycle and a11y fields — id, variant (drives ARIA role), transition, dismiss timer, hover state. Everything else lives in your payload and is rendered by your renderEntry callback. Ui.Toast.make(PayloadSchema) returns a module with Model, show, view, and the rest bound to your payload type.
See it in an app
Check out how Toast is wired up in a real Foldkit app.
Click a variant to push a toast onto the stack. Hover a toast to pause its auto-dismiss; move away and the timer restarts.
// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt — fit them into your own Model, init, Message,
// update, and view definitions.
import { Option, Schema as S } from 'effect'
import { Command, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, Href, OnClick, a, button, div, p } from './html'
// Define the payload shape for your toast. The Toast component owns only
// lifecycle + a11y fields (id, variant, transition, dismiss timer, hover
// state). The payload is yours — whatever you can encode in a Schema:
const ToastPayload = S.Struct({
bodyText: S.String,
maybeLink: S.OptionFromSelf(S.Struct({ href: S.String, text: S.String })),
})
// Bind a Toast module to your payload schema:
export const Toast = Ui.Toast.make(ToastPayload)
// Add Toast.Model to your app Model:
const Model = S.Struct({
toast: Toast.Model,
// ...your other fields
})
// In your init function, initialize it:
const init = () => [
{
toast: Toast.init({ id: 'app-toast' }),
// ...your other fields
},
[],
]
// Embed the Toast Message in your parent Message, plus any domain Messages
// that should push a toast:
const GotToastMessage = m('GotToastMessage', { message: Toast.Message })
const ClickedSave = m('ClickedSave')
// Inside your update's M.tagsExhaustive({...}), delegate Toast's own Messages
// and call Toast.show from any domain branch that should surface a notification:
GotToastMessage: ({ message }) => {
const [nextToasts, commands] = Toast.update(model.toast, message)
return [
evo(model, { toast: () => nextToasts }),
commands.map(
Command.mapEffect(Effect.map(message => GotToastMessage({ message }))),
),
]
}
ClickedSave: () => {
const [nextToasts, commands] = Toast.show(model.toast, {
variant: 'Success',
payload: {
bodyText: 'Changes saved',
maybeLink: Option.some({ href: '/changes', text: 'View' }),
},
})
return [
evo(model, { toast: () => nextToasts }),
commands.map(
Command.mapEffect(Effect.map(message => GotToastMessage({ message }))),
),
]
}
// In your view, render the Toast container once at the app root. Provide a
// renderEntry callback that lays out each entry from its payload — the
// component handles the <li> wrapper, hover-to-pause, and enter/leave
// animations.
Toast.view({
model: model.toast,
position: 'BottomRight',
toParentMessage: message => GotToastMessage({ message }),
renderEntry: (entry, handlers) =>
div(
[Class('flex items-start gap-3 rounded-lg border bg-white p-3 shadow')],
[
div(
[Class('flex-1')],
[
p([Class('font-semibold text-sm')], [entry.payload.bodyText]),
...Option.match(entry.payload.maybeLink, {
onNone: () => [],
onSome: ({ href, text }) => [
a([Class('text-sm underline'), Href(href)], [text]),
],
}),
],
),
button([OnClick(handlers.dismiss)], ['Close']),
],
),
entryClassName: 'w-80',
})Toast is headless. The container gets position: fixed and flex-column layout from the component (so entries stack correctly for each position); every other visual decision lives in your renderEntry callback and your entryClassName. Use data-variant on the entry to drive per-variant styling.
| Attribute | Condition |
|---|---|
data-variant | Present on each entry, with the variant value (Info, Success, Warning, Error). Use for per-variant CSS. |
data-enter | Present on an entry while its enter animation runs. |
data-leave | Present on an entry while its leave animation runs. |
data-closed | Present on an entry at the closed extreme of its enter or leave animation. Pair with data-enter or data-leave to drive the starting and ending CSS states. |
data-transition | Present on an entry while either animation runs. |
The container is a role="region" with aria-live="polite", always rendered (even when empty) so screen readers observe the live region from page load. Individual entries receive role="status" for Info and Success variants, role="alert" for Warning and Error. Auto-dismiss pauses on pointer hover.
Configuration object passed to Toast.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the toast container. |
defaultDuration | Duration.DurationInput | Duration.seconds(4) | Auto-dismiss duration applied to any show() call that does not provide its own duration or pass sticky: true. Accepts any Effect Duration input; a bare number is interpreted as milliseconds. |
Input shape for Toast.show(model, input).
| Name | Type | Default | Description |
|---|---|---|---|
payload | A (your payload type) | - | Content for this entry, in whatever shape you supplied to Toast.make(). The component never reads it; it flows through to your renderEntry callback. |
variant | 'Info' | 'Success' | 'Warning' | 'Error' | 'Info' | Semantic category. Maps to data-variant for styling and to role=status (Info, Success) or role=alert (Warning, Error) for accessibility. The only content-adjacent field the component owns — everything else is in payload. |
duration | Duration.DurationInput | - | Overrides the container's defaultDuration for this entry. Ignored when sticky: true. |
sticky | boolean | false | When true, the entry never auto-dismisses. The user must close it manually. |
Configuration object passed to Toast.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | Toast.Model | - | The toast container state from your parent Model. |
position | 'TopLeft' | 'TopCenter' | 'TopRight' | 'BottomLeft' | 'BottomCenter' | 'BottomRight' | - | Where the toast viewport is anchored on the screen. |
toParentMessage | (childMessage: Dismissed | HoveredEntry | LeftEntry) => ParentMessage | - | Wraps the subset of Toast Messages that fire from DOM events in your parent Message type. |
renderEntry | (entry: typeof Toast.Entry.Type, handlers: { dismiss: ParentMessage }) => Html | - | Renders each entry from its lifecycle fields (id, variant, transition) and its payload (your shape). The component wraps the return in an <li> with role, lifecycle handlers, and transition data attributes. Wire handlers.dismiss to a close button's OnClick. |
ariaLabel | string | 'Notifications' | aria-label on the container region. |
className | string | - | CSS class for the container <ol>. |
entryClassName | string | - | CSS class applied to every <li> entry. |
Helper functions for driving toasts from parent update handlers, returning [Model, Commands].
| Name | Type | Default | Description |
|---|---|---|---|
show | (model: Model, input: ShowInput) => [Model, Commands] | - | Adds a new toast entry. Call this from any parent update handler that needs to surface a notification. Returns the next model plus commands for the enter animation and the auto-dismiss timer. |
dismiss | (model: Model, entryId: string) => [Model, Commands] | - | Begins dismissing a specific entry. Safe to call for an entry that is already leaving or has been removed. |
dismissAll | (model: Model) => [Model, Commands] | - | Begins dismissing every currently-visible entry. |