Toast
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add overlays/toastPlain-CSS theme — no build step
npx astrocraft-ui add overlays/toast --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add overlays/toast --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add overlays/toast --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add overlays/toast --theme tailwind --bridge lumosLive demo
Toast
The Toaster is already in the page — that is the point of it. Showing a toast istoast.hidden = false, the library's ordinary visibility contract, and the region announces it because it existed first. Hover the stack and the auto-dismiss timers pause; the error toast has duration=0and waits for you.
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `Toast.astro` | `toast` | `data-variant`: `info` · `success` · `warning` · `error` | — | — |
| `ToastAction.astro` | `toast-action` | `data-variant`: `primary` · `secondary` · `outline` · `ghost` `data-size`: `sm` · `md` · `lg` | — | — |
| `ToastClose.astro` | `toast-close` | — | — | — |
| `ToastDescription.astro` | `toast-description` | — | — | — |
| `ToastTitle.astro` | `toast-title` | — | — | — |
| `Toaster.astro` | `toaster` | `data-politeness`: `polite` · `assertive` `data-position`: `top-right` · `top-left` · `bottom-right` · `bottom-left` · `top` · `bottom` | — | — |
Source
What the command copies — 7 files, alias-free so the copy lands standing alone. This is the whole component, shown without its comments; the files you install keep them, with the reasoning behind every rule.
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"div"> & {
variant?: "info" | "success" | "warning" | "error";
duration?: number;
};
const { variant = "info", duration = 5000, class: className, ...rest } = Astro.props;
---
<div class={className} data-slot="toast" data-variant={variant} data-duration={duration} {...rest}>
<slot />
</div>
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"button"> & {
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
};
const { variant = "outline", size = "sm", class: className, ...rest } = Astro.props;
---
<button
type="button"
data-slot="toast-action"
data-variant={variant}
data-size={size}
class={className}
{...rest}
>
<slot />
</button>
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"button"> & { label?: string };
const { label = "Dismiss", class: className, ...rest } = Astro.props;
---
<button type="button" aria-label={label} data-slot="toast-close" class={className} {...rest}>
<slot />
</button>
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"p">;
const { class: className, ...rest } = Astro.props;
---
<p class={className} data-slot="toast-description" {...rest}><slot /></p>
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"p">;
const { class: className, ...rest } = Astro.props;
---
<p class={className} data-slot="toast-title" {...rest}><slot /></p>
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"div"> & {
label?: string;
politeness?: "polite" | "assertive";
position?: "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top" | "bottom";
};
const {
label = "Notifications",
politeness = "polite",
position = "bottom-right",
class: className,
...rest
} = Astro.props;
---
<div
role="region"
aria-label={label}
aria-live={politeness}
class={className}
data-slot="toaster"
data-position={position}
{...rest}
>
<slot />
</div>
<script>
import { onReadyOnce } from "../../_once";
const timers = new WeakMap<
HTMLElement,
{ id: ReturnType<typeof setTimeout>; duration: number }
>();
const paused = new WeakSet<HTMLElement>();
function clear(toast: HTMLElement) {
clearTimeout(timers.get(toast)?.id);
timers.delete(toast);
}
function dismiss(toast: HTMLElement) {
clear(toast);
toast.hidden = true;
toast.dispatchEvent(new Event("toast:dismiss", { bubbles: true }));
}
function toastsIn(region: HTMLElement): HTMLElement[] {
return [...region.querySelectorAll<HTMLElement>('[data-slot="toast"]')];
}
function sync(region: HTMLElement) {
for (const toast of toastsIn(region)) {
const duration = Number(toast.dataset.duration);
if (toast.hidden || paused.has(region) || !duration) {
clear(toast);
} else if (timers.get(toast)?.duration !== duration) {
clear(toast);
timers.set(toast, { id: setTimeout(() => dismiss(toast), duration), duration });
}
}
}
function wire(region: HTMLElement) {
const pause = () => {
paused.add(region);
sync(region);
};
const resume = () => {
paused.delete(region);
sync(region);
};
region.addEventListener("pointerenter", pause);
region.addEventListener("pointerleave", resume);
region.addEventListener("focusin", pause);
region.addEventListener("focusout", resume);
region.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return;
const control = event.target.closest('[data-slot="toast-close"], [data-slot="toast-action"]');
const toast = control?.closest<HTMLElement>('[data-slot="toast"]');
if (toast) dismiss(toast);
});
new MutationObserver(() => sync(region)).observe(region, {
childList: true,
subtree: true,
attributeFilter: ["hidden", "data-duration"],
});
sync(region);
}
onReadyOnce('[data-slot="toaster"]', wire);
</script>
import Toast from "./Toast.astro";
import ToastAction from "./ToastAction.astro";
import ToastClose from "./ToastClose.astro";
import ToastDescription from "./ToastDescription.astro";
import Toaster from "./Toaster.astro";
import ToastTitle from "./ToastTitle.astro";
export { Toast, ToastAction, ToastClose, ToastDescription, Toaster, ToastTitle };
export default Toaster;
What you get
The component source, copied into your project by npx astrocraft-ui add overlays/toast — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.