Zero-dependency toast notifications for React, built on the Popover API with CSS-only animations.
- Zero runtime dependencies — React 18+ is the only peer dep
- Top-layer rendering — uses
popover="manual"so toasts always appear above modals and drawers without z-index management - CSS-only animations — entrance via
@starting-style, exit viatransition+transitionend; no animation library needed - Accessible —
role="alert"for errors,role="status"for others;prefers-reduced-motionrespected
| API | Support |
|---|---|
popover="manual" |
Chrome 114+, Safari 17+, Firefox 125+ |
@starting-style (entrance animation) |
Chrome 117+, Safari 17.5+ |
npm install popover-toast<Toaster /> は 常にマウントされているコンポーネント に一度だけ置いてください。ページコンポーネントに置くとページ遷移時にアンマウントされ、トーストが表示されなくなります。
// src/App.tsx
import { Toaster } from 'popover-toast'
import 'popover-toast/toast.css'
export default function App() {
return (
<>
<YourApp />
<Toaster position="bottom-right" />
</>
)
}// app/layout.tsx
import type { Metadata } from 'next'
import { Toaster } from 'popover-toast'
import 'popover-toast/toast.css'
export const metadata: Metadata = {
title: 'My App',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
<Toaster position="bottom-right" />
</body>
</html>
)
}Server Components から toast() を呼ぶことはできません。Client Components ('use client') またはイベントハンドラから呼んでください。
// app/some-page/page.tsx
'use client'
import { toast } from 'popover-toast'
export default function SomePage() {
return (
<button onClick={() => toast.success('保存しました')}>
保存
</button>
)
}import { toast } from 'popover-toast'
toast('Message sent')
toast.success('Saved successfully')
toast.error('Something went wrong')
toast.warning('Unsaved changes will be lost')
toast.info('New version available')toast.success('File deleted', {
description: 'The file has been moved to trash.',
action: {
label: 'Undo',
onClick: () => toast.success('Deletion undone'),
},
})Displays a loading toast that updates to success or error when the promise settles.
toast.promise(saveData(), {
loading: 'Saving…',
success: 'Saved!',
error: (err) => `Failed: ${err.message}`,
})toast('Deployment started', {
icon: '🚀',
duration: 8000,
})const id = toast('Processing…', { duration: Infinity })
// later
toast.dismiss(id) // dismiss one
toast.dismiss() // dismiss allPass the same id to replace a toast in place.
const id = toast('Uploading…', { duration: Infinity })
toast.success('Upload complete!', { id })| Prop | Type | Default | Description |
|---|---|---|---|
position |
ToastPosition |
"bottom-right" |
Where toasts appear |
duration |
number |
4000 |
Auto-dismiss delay in ms |
gap |
number |
8 |
Gap between toasts in px |
offset |
number | string |
"1rem" |
Distance from the viewport edge |
maxToasts |
number |
5 |
Maximum number of toasts shown at once |
ToastPosition values: "top-left" "top-center" "top-right" "bottom-left" "bottom-center" "bottom-right"
| Option | Type | Description |
|---|---|---|
id |
string |
Provide a fixed ID to update an existing toast |
description |
ReactNode |
Secondary text below the message |
action |
{ label, onClick } |
Action button |
icon |
ReactNode |
Custom icon (overrides the default type icon) |
duration |
number |
Per-toast override in ms; Infinity to persist |
All visual properties are exposed as CSS custom properties on :root.
:root {
--toast-offset: 1rem; /* distance from viewport edge */
--toast-gap: 8px; /* gap between toasts */
--toast-min-width: 280px;
--toast-max-width: 400px;
--toast-padding: 0.75rem 1rem;
--toast-border-radius: 0.5rem;
--toast-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
--toast-font-size: 0.875rem;
/* Default (type="default") */
--toast-bg: #fff;
--toast-color: #1a1a1a;
--toast-border: rgba(0, 0, 0, 0.08);
/* Per-type overrides */
--toast-success-bg: #f0fdf4;
--toast-success-color: #166534;
--toast-success-border: #bbf7d0;
--toast-success-icon: #22c55e;
--toast-error-bg: #fef2f2;
--toast-error-color: #991b1b;
--toast-error-border: #fecaca;
--toast-error-icon: #ef4444;
--toast-warning-bg: #fffbeb;
--toast-warning-color: #92400e;
--toast-warning-border: #fde68a;
--toast-warning-icon: #f59e0b;
--toast-info-bg: #eff6ff;
--toast-info-color: #1e40af;
--toast-info-border: #bfdbfe;
--toast-info-icon: #3b82f6;
}Top-layer rendering via Popover API
The <Toaster> renders a <div popover="manual"> element. When a toast is added, showPopover() promotes it to the browser's top layer — the same layer used by <dialog>. This means toasts always appear above everything else in the document without any z-index management.
State management with useSyncExternalStore
Toast state lives in a plain module-level array outside of React. The toast() function can be called from anywhere (event handlers, async functions, server actions). useSyncExternalStore connects this external store to React's rendering pipeline in a Concurrent Mode-safe way.
CSS-only animations
- Entrance:
@starting-styledefines the pre-insertion state (opacity: 0,transform: translateY). The browser transitions from this state to the normal style as soon as the element is added to the DOM. - Exit: React sets
data-dismissingon the element, triggering a CSS transition toopacity: 0andmax-height: 0. Once the transition ends, atransitionendlistener removes the toast from the store.
MIT