Toast
Stateless toast in the top layer. Caller owns open state, timing, and dismissal (render a close Button in children, or set a timeout). For a queue + toast() sugar, use Sonner — the library intentionally does not ship an imperative API.
プレビュー
Changes saved.
インストール
npx @joinclass/axiom-ui add toast toast.tsx を ./src/components/ にコピーします。以降のコードはあなたのものです。
Props
| Prop | 説明 |
|---|---|
| open | Required boolean. Flip to show/hide. The native popover=manual mode means only your code closes it — Escape / outside-click do NOT. |
| role | Optional. Default "status" (polite announcement). Use "alert" for errors that must interrupt the screen reader. |
| ...native | Every ComponentPropsWithRef<'div'> attribute passes through (children, aria-*, ref, ...). |
| className | Escape hatch: appended after the defaults. Position defaults use `!` to beat the browser's popover UA styles (margin:auto; inset:0); override with your own `!top-4 !left-4` etc. |
使用例
Auto-dismiss after 4 seconds
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return;
const id = setTimeout(() => setOpen(false), 4000);
return () => clearTimeout(id);
}, [open]);
<>
<Button onClick={() => setOpen(true)}>Save</Button>
<Toast open={open}>Changes saved.</Toast>
</> Error toast with manual close
<Toast open={error !== null} role="alert" className="border-red-300 bg-red-50">
<div className="flex items-start justify-between gap-4">
<p>{error}</p>
<Button onClick={() => setError(null)} className="!bg-transparent !text-zinc-500">✕</Button>
</div>
</Toast> アクセシビリティ
- role="status" implies aria-live="polite" — announced after the current speech.
- role="alert" implies aria-live="assertive" — interrupts. Reserve for errors.
- Manual mode: Escape does NOT close the toast. Provide a visible dismiss control (usually a close Button in children).
- Requires the native Popover API (Chrome/Edge 114+, Safari 17.4+, Firefox 125+). On older browsers renders as an inline <div> — still functional and announced.
状態
closedopen
ソースコード
toast.tsx の全ソース。CLI がコピーするコードそのものです。
/**
* Stateless toast notification. Renders in the browser's top layer
* via <div popover="manual"> — no z-index stacking. Timing, queue,
* and dismissal are the caller's responsibility (this is why the
* library has no `toast()` global API; for that, use Sonner).
* Set role="alert" for errors that must interrupt; the default
* role="status" announces politely.
*/
import { useEffect, useRef, type ComponentPropsWithRef } from "react";
export type ToastProps = ComponentPropsWithRef<"div"> & {
open: boolean;
};
export function Toast({ open, className, role = "status", ...props }: ToastProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = ref.current;
if (!element || typeof element.showPopover !== "function") return;
let isOpen = false;
try { isOpen = element.matches(":popover-open"); } catch { /* pre-popover browser */ }
if (open && !isOpen) element.showPopover();
else if (!open && isOpen) element.hidePopover();
}, [open]);
return (
<div
ref={ref}
popover="manual"
role={role}
className={
"!fixed !top-auto !right-4 !bottom-4 !left-auto !m-0 max-w-sm rounded-md border border-zinc-200 bg-white p-4 text-sm text-zinc-900 shadow-lg" +
(className ? " " + className : "")
}
{...props}
/>
);
}
export const manifest = {
intent:
'Stateless toast in the top layer. Caller owns open state, timing, and dismissal (render a close Button in children, or set a timeout). For a queue + toast() sugar, use Sonner — the library intentionally does not ship an imperative API.',
props: {
open: "Required boolean. Flip to show/hide. The native popover=manual mode means only your code closes it — Escape / outside-click do NOT.",
role: 'Optional. Default "status" (polite announcement). Use "alert" for errors that must interrupt the screen reader.',
"...native":
"Every ComponentPropsWithRef<'div'> attribute passes through (children, aria-*, ref, ...).",
className:
"Escape hatch: appended after the defaults. Position defaults use `!` to beat the browser's popover UA styles (margin:auto; inset:0); override with your own `!top-4 !left-4` etc.",
},
states: ["closed", "open"],
a11y: [
'role="status" implies aria-live="polite" — announced after the current speech.',
'role="alert" implies aria-live="assertive" — interrupts. Reserve for errors.',
"Manual mode: Escape does NOT close the toast. Provide a visible dismiss control (usually a close Button in children).",
"Requires the native Popover API (Chrome/Edge 114+, Safari 17.4+, Firefox 125+). On older browsers renders as an inline <div> — still functional and announced.",
],
examples: [
{
title: "Auto-dismiss after 4 seconds",
code: 'const [open, setOpen] = useState(false);\nuseEffect(() => {\n if (!open) return;\n const id = setTimeout(() => setOpen(false), 4000);\n return () => clearTimeout(id);\n}, [open]);\n<>\n <Button onClick={() => setOpen(true)}>Save</Button>\n <Toast open={open}>Changes saved.</Toast>\n</>',
},
{
title: "Error toast with manual close",
code: '<Toast open={error !== null} role="alert" className="border-red-300 bg-red-50">\n <div className="flex items-start justify-between gap-4">\n <p>{error}</p>\n <Button onClick={() => setError(null)} className="!bg-transparent !text-zinc-500">✕</Button>\n </div>\n</Toast>',
},
],
} as const;