Toast new
import { toast } from "@katanakit/ui";
toast.success("Profile saved.");
toast.error("Upload failed.", { duration: 0 });The toast export is a lazy facade over a per-page singleton: importing it during SSR is safe, and document is only touched when a method actually runs.
toastClass() and toastContainerClass() return plain class strings for SSR markup, useToast() builds a single detached element when you own the lifecycle yourself, and useToastStore() is the same store as toast under the use* accessor convention.
No JSX, no framework runtime — the store talks to document.body directly, so the same code works from Astro, Nuxt, plain Node scripts or a static page.
Names
The call shape follows react-toastify, but the option names are ours. duration: 0 reads as "pin it open"; react-toastify's autoClose: false reads like a boolean.
Variants
Five types. error escalates its own live region to role="alert"; everything else is a polite role="status". loading is indeterminate and stays pinned until you dismiss or update it.
import { toast } from "@katanakit/ui";
toast.info("Sync started.");
toast.success("Profile saved.");
toast.warning("Quota at 90%.");
toast.error("Upload failed.");
toast.loading("Uploading 3 files…");The loading → success pattern
update() swaps the content in place and keeps the id, position and stacking order, so a progress toast never jumps around the screen.
const handle = toast.loading("Uploading 3 files…");
// later — the stack keeps the same slot
handle.update({ type: "success", message: "3 files uploaded." });Duration
Default is 5000 ms. error gets 8000 ms so it is not missed, and loading gets 0 — pinned open. Pass duration: 0 to pin any type.
| Type | Default | Rationale |
|---|---|---|
info | 5000 ms | Reads at a glance. |
success | 5000 ms | Confirmation does not need extra time. |
warning | 5000 ms | |
error | 8000 ms | Should not vanish before it is read. |
loading | pinned | Only you know when the work finished. |
Pause on hover and focus
The countdown freezes on mouseenter and on focusin, and the progress bar freezes with it via animation-play-state — the bar and the JS timer never desync. Keyboard users get the same courtesy as mouse users.
Positions
Six anchors: top-right (default), top-center, top-left, bottom-right, bottom-center, bottom-left. Each position gets its own stack, created lazily.
toast.info("Top right", { position: "top-right" });
toast.info("Bottom center", { position: "bottom-center" });Options
| Option | Type | Default | Notes |
|---|---|---|---|
type | "info" | "success" | "warning" | "error" | "loading" | "info" | Colors and ARIA role. |
title | string | — | Bold heading line. |
message | string | — | Body text. |
size | "sm" | "md" | "lg" | "md" | |
position | six-anchor union | "top-right" | Stack to mount into. |
duration | number (ms) | per type | 0 pins the toast open. |
dismissible | boolean | true | Renders the close button. |
closeLabel | string | "Dismiss notification" | Accessible name for that button. |
onClose | (id: string) => void | — | Called once the toast leaves the DOM. |
Contract
Every component in the kit follows one shape. Pure builders return a class string and touch nothing; factories build a DOM element and need a browser.
| Surface | Import | Needs a DOM |
|---|---|---|
| Pure class builder | toastClass | No — safe during SSR |
| Element factory | useToast | Yes — throws with a pointer to the pure surface |
// Server and templates — pure
<div class={ toastClass({ /* … */ }) }></div>
// Browser — imperative
document.body.append(useToast({ /* … */ }));Class names are unprefixed. The root class is toast, not kk-toast, and every component rule is wrapped in @layer katana.
That matters because these names collide 1:1 with daisyUI. Unlayered CSS always beats layered CSS regardless of specificity or import order, so loading both is deterministic instead of "whoever came last". See why.
Store API
| Method | Returns | Notes |
|---|---|---|
show(options?) | ToastHandle | Full control over every option. |
info(message, opts?) | ToastHandle | Sugar for show({ type: "info", message }). |
success(message, …) | ToastHandle | |
warning(message, …) | ToastHandle | |
error(message, …) | ToastHandle | Assertive role. |
loading(message, …) | ToastHandle | Pinned open. |
dismiss(id?) | void | One by id, or every toast when omitted. |
clear() | void | Every toast across all positions. |
ToastHandle is { id, update(partial), dismiss() }. toast and useToastStore() hand back the same singleton object, so you can pass the notifier around without wiring a local const:
import { useToastStore } from "@katanakit/ui";
function notifyUser(notifier = useToastStore()) {
notifier.success("Done.");
}For call sites that cannot import a factory at all — a DI container, a framework plugin wiring — ToastService.getInstance() returns the identical store:
ToastService.getInstance().success("Done.");Pure helpers
| Helper | Returns | Use for |
|---|---|---|
toastClass(options) | string | class={toastClass({ type: "success" })} |
toastContainerClass() | string | The stack wrapper in a template. |
Both are pure and SSR-safe: no DOM, no state, just stable strings.
Accessibility
- The stack is
role="region"+aria-live="polite"+aria-label="Notifications". - Each toast is
role="status", escalating torole="alert"forerror. - Status icons are inline SVG marked
aria-hidden="true"— the message carries the meaning, so the glyph is decorative. - The countdown bar is
aria-hidden="true"; it is a timing cue, not content. prefers-reduced-motion: reduceremoves every animation and the slide-in transform.
SSR
toast.*, useToast() and useToastContainer() need a DOM and throw with a pointer to the fix — but only when called, never on import. On the server use toastClass() and toastContainerClass(), which are pure.
---
// Astro: markup from the pure surface, behaviour from a client script
import { toastClass, toastContainerClass } from "@katanakit/ui";
---
<div class={toastContainerClass({ position: "top-right" })} role="region" aria-label="Notifications">
<div class={toastClass({ type: "success" })} role="status">
<div class="toast__content"><p class="toast__message">Deployed.</p></div>
</div>
</div>
<script>
import { toast } from "@katanakit/ui";
toast.success("Deployed.");
</script>