Skip to content
0

Toast new ​

ts
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.

ts
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.

ts
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.

TypeDefaultRationale
info5000 msReads at a glance.
success5000 msConfirmation does not need extra time.
warning5000 ms
error8000 msShould not vanish before it is read.
loadingpinnedOnly 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.

ts
toast.info("Top right", { position: "top-right" });
toast.info("Bottom center", { position: "bottom-center" });

Options ​

OptionTypeDefaultNotes
type"info" | "success" | "warning" | "error" | "loading""info"Colors and ARIA role.
titlestring—Bold heading line.
messagestring—Body text.
size"sm" | "md" | "lg""md"
positionsix-anchor union"top-right"Stack to mount into.
durationnumber (ms)per type0 pins the toast open.
dismissiblebooleantrueRenders the close button.
closeLabelstring"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.

SurfaceImportNeeds a DOM
Pure class buildertoastClassNo — safe during SSR
Element factoryuseToastYes — 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 ​

MethodReturnsNotes
show(options?)ToastHandleFull control over every option.
info(message, opts?)ToastHandleSugar for show({ type: "info", message }).
success(message, …)ToastHandle
warning(message, …)ToastHandle
error(message, …)ToastHandleAssertive role.
loading(message, …)ToastHandlePinned open.
dismiss(id?)voidOne by id, or every toast when omitted.
clear()voidEvery 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:

ts
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:

ts
ToastService.getInstance().success("Done.");

Pure helpers ​

HelperReturnsUse for
toastClass(options)stringclass={toastClass({ type: "success" })}
toastContainerClass()stringThe 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 to role="alert" for error.
  • 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: reduce removes 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
---
// 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>

Released under the MIT License.