Toast
<oblyx-toast-viewport> is a singleton container you place once in server-rendered markup, typically just before </body>. Every <oblyx-toast> it stacks and announces is an ordinary child element: there is no JavaScript array tracking the stack anywhere, the DOM child list of the viewport is the queue.
A real page renders exactly one viewport. The examples on this page each render their own, and each sets a distinct position purely so they don't stack on top of one another in the corner of your actual browser window: the viewport promotes itself to the page's top layer on connect, so its corner is the real screen corner, not a spot inside the preview box below.
Basic toast
<oblyx-toast-viewport>
<oblyx-toast type="success">Changes saved.</oblyx-toast>
</oblyx-toast-viewport>oblyxToastViewport {
oblyxToast(type = OblyxToastType.SUCCESS) { +"Changes saved." }
}Three ways a toast arrives
A toast reaches the viewport one of three ways, and all three are handled identically because each <oblyx-toast> is fully self-sufficient: its own timer, its own role, its own dismiss button. None of the three routes needs to tell the viewport anything beyond inserting the element.
The first is present in the page's own initial HTML, exactly like the basic example above: a server renders the viewport with a toast already inside it, for example after a redirect following a form submission. The second, and the one this library's server-rendered audience reaches for most, is an HTMX out-of-band swap: a response to some other request carries a <oblyx-toast hx-swap-oob="beforeend:#viewport-id"> fragment with no Oblyx-specific wiring beyond that one attribute, and HTMX appends it to the viewport regardless of what the request's own hx-target was. The third is the exported showToast() helper, covered near the bottom of this page, for the pure-client case where nothing rendered on a server at all.
Emitting a toast from a server-rendered fragment
The example below performs a real hx-get, a real fetch and a real out-of-band swap: open your browser's network panel and watch it happen. The response is a fixed file on this site rather than something a server rendered for that specific request, since this docs site has no backend of its own; the fragment itself is exactly the shape a real Ktor route would return, one <oblyx-toast hx-swap-oob> element and nothing else. Click the button more than once to see several toasts stack, and to see max-visible evict the oldest once there are more than three.
Toast delivered by an out-of-band swap
The HTML tab's hx-get points at a static file so the live preview above actually works; the Kotlin tab uses a route path a real Ktor server would serve dynamically.
<oblyx-toast-viewport id="ox-doc-toast-viewport" position="top-end" max-visible="3"></oblyx-toast-viewport>
<button type="button" hx-get="/examples/feedback/toast-oob.html" hx-swap="none">
Check for updates
</button>oblyxToastViewport(position = OblyxToastViewportPosition.TOP_END, maxVisible = 3) {
id = "ox-doc-toast-viewport"
}
button {
// A plain <button> here, not oblyxButton: this row needs the raw hx-get
// attribute and oblyxButton's own attributes don't cover it directly.
attributes["hx-get"] = "/fragments/check-for-updates"
attributes["hx-swap"] = "none"
+"Check for updates"
}Statuses
type drives the icon, the accent color, and which live region the toast announces through:info, success, warning, or error, the same four-value vocabulary alert already established. The four below use the default five-second timer, so they'll auto-dismiss shortly after this page loads; reload to see them again.
Toast statuses
<oblyx-toast-viewport position="top-start">
<oblyx-toast type="info">A new version is available.</oblyx-toast>
<oblyx-toast type="success">Changes saved.</oblyx-toast>
<oblyx-toast type="warning">Your session expires in five minutes.</oblyx-toast>
<oblyx-toast type="error">Something went wrong. Try again.</oblyx-toast>
</oblyx-toast-viewport>oblyxToastViewport(position = OblyxToastViewportPosition.TOP_START) {
oblyxToast(type = OblyxToastType.INFO) { +"A new version is available." }
oblyxToast(type = OblyxToastType.SUCCESS) { +"Changes saved." }
oblyxToast(type = OblyxToastType.WARNING) { +"Your session expires in five minutes." }
oblyxToast(type = OblyxToastType.ERROR) { +"Something went wrong. Try again." }
}Duration and persistence
duration and persistent are both always explicit, rather than persistent defaulting differently per type: a reader of the markup can see the whole timing decision without also knowing an implicit rule tied to severity. Reach for persistent on a toast that carries information a user needs to act on deliberately, not on anything a user would reasonably expect to just go away.
Custom duration and persistent
<oblyx-toast-viewport position="bottom-start">
<oblyx-toast type="info" duration="10000">Auto-dismisses after ten seconds.</oblyx-toast>
<oblyx-toast type="error" persistent>Persistent: stays until dismissed by hand.</oblyx-toast>
</oblyx-toast-viewport>oblyxToastViewport(position = OblyxToastViewportPosition.BOTTOM_START) {
oblyxToast(type = OblyxToastType.INFO, duration = 10000) { +"Auto-dismisses after ten seconds." }
oblyxToast(type = OblyxToastType.ERROR, persistent = true) { +"Persistent: stays until dismissed by hand." }
}Standalone toast
A bare <oblyx-toast> with no <oblyx-toast-viewport> ancestor still renders its full visible content and still auto-dismisses on its own timer. What it cannot do without a viewport is write into the two persistent, empty live regions the viewport holds for a reliable screen-reader announcement (see below); its own visible role is then the only announcement mechanism, exactly like any other role="status" element placed directly in a page. This is a real, disclosed accessibility difference, not a silent one: reach for a standalone toast only for a case where mounting the shared viewport is genuinely impractical.
Toast with no viewport
<oblyx-toast type="warning" persistent>
Standalone: no oblyx-toast-viewport ancestor, still fully rendered.
</oblyx-toast>oblyxToast(type = OblyxToastType.WARNING, persistent = true) {
+"Standalone: no oblyx-toast-viewport ancestor, still fully rendered."
}Why a toast is announced
Many screen readers only announce a live region that was already present and empty, then mutated, not one discovered already populated. <oblyx-toast-viewport> renders two persistent, initially empty live regions, one polite and one assertive, and each connecting toast writes its own text into whichever one matches its severity a tick after connecting, so the mutation itself is what gets announced.
Emitting a toast from plain JavaScript
showToast() is the pure-client route, for a case with nothing for a server to render at all, for example a copy-to-clipboard confirmation. It finds or lazily creates the page's <oblyx-toast-viewport>, builds one <oblyx-toast> element, and appends it: the same mechanism as the other two routes, not a second parallel one. It returns a function that dismisses the created toast early.
import { showToast } from 'oblyx';
const dismiss = showToast({ type: 'success', body: 'Link copied to clipboard.' });
// dismiss() closes it early, if you need to.Reference
<oblyx-toast-viewport>
| Attribute | Type | Default | Description |
|---|---|---|---|
position | bottom-endbottom-starttop-endtop-start | bottom-end | Which screen corner the stack anchors to. |
max-visible | number | undefined | – | Caps how many toasts are visible at once; the oldest are dismissed as newer ones arrive. Unset (the default) means no cap. |
<oblyx-toast>
| Attribute | Type | Default | Description |
|---|---|---|---|
type | infosuccesswarningerror | info | Drives the icon, the accent color, and which live region (and which ARIA role) this toast announces through. |
duration | number | DEFAULT_DURATION_MS | Auto-dismiss timeout in milliseconds. Ignored when persistent is set. |
persistent | boolean | false | When set, this toast never auto-dismisses; duration is ignored. |
Events
| Event | Type | Description |
|---|---|---|
dismiss | Event | Fired when this toast is dismissed, whether by its own timer, a click on its dismiss button, or eviction by the viewport's max-visible cap. Not cancelable; there is no way to veto dismissal from a listener. |