Skip to content

kentem-yu-ogihara/popover-toast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

popover-toast

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 via transition + transitionend; no animation library needed
  • Accessiblerole="alert" for errors, role="status" for others; prefers-reduced-motion respected

Browser support

API Support
popover="manual" Chrome 114+, Safari 17+, Firefox 125+
@starting-style (entrance animation) Chrome 117+, Safari 17.5+

Installation

npm install popover-toast

Setup

<Toaster />常にマウントされているコンポーネント に一度だけ置いてください。ページコンポーネントに置くとページ遷移時にアンマウントされ、トーストが表示されなくなります。

React (Vite など)

// src/App.tsx
import { Toaster } from 'popover-toast'
import 'popover-toast/toast.css'

export default function App() {
  return (
    <>
      <YourApp />
      <Toaster position="bottom-right" />
    </>
  )
}

Next.js App Router

// 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>
  )
}

Usage

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')

With description and action

toast.success('File deleted', {
  description: 'The file has been moved to trash.',
  action: {
    label: 'Undo',
    onClick: () => toast.success('Deletion undone'),
  },
})

Promise

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}`,
})

Custom icon and duration

toast('Deployment started', {
  icon: '🚀',
  duration: 8000,
})

Programmatic dismiss

const id = toast('Processing…', { duration: Infinity })

// later
toast.dismiss(id)   // dismiss one
toast.dismiss()     // dismiss all

Updating an existing toast

Pass the same id to replace a toast in place.

const id = toast('Uploading…', { duration: Infinity })
toast.success('Upload complete!', { id })

<Toaster> props

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"

toast() options

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

Customization

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;
}

How it works

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-style defines 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-dismissing on the element, triggering a CSS transition to opacity: 0 and max-height: 0. Once the transition ends, a transitionend listener removes the toast from the store.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors