Toast Portal
Notification / Toast Portal
Toast notifications appear fixed at a corner of the viewport. A Portal renders them into a
dedicated container at the body level so they’re always visible regardless of the current
page scroll or component depth.
ToastContext.jsx: Global Toast Manager
import { createContext, useContext, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
const ToastContext = createContext(null);
let idCounter = 0;
export function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const addToast = useCallback((message, type = 'info', duration = 3000) => {
const id = ++idCounter;
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, duration);
}, []);
const removeToast = useCallback((id) => {
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
{createPortal(
<div
aria-live="polite"
aria-atomic="false"
style={{
position: 'fixed',
bottom: '1rem',
right: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
zIndex: 99999
}}
>
{toasts.map(toast => (
<div
key={toast.id}
role="alert"
style={{
padding: '0.75rem 1.25rem',
background: toast.type === 'error' ? '#dc3545' : toast.type === 'success' ? '#28a745' : '#17a2b8',
color: '#fff',
borderRadius: '6px',
cursor: 'pointer'
}}
onClick={() => removeToast(toast.id)}
>
{toast.message}
</div>
))}
</div>,
document.body
)}
</ToastContext.Provider>
);
}
export function useToast() {
return useContext(ToastContext);
}
App.jsx: Using the Toast System
import { ToastProvider, useToast } from './ToastContext';
function Inner() {
const { addToast } = useToast();
return (
<div>
<button onClick={() => addToast('Operation successful!', 'success')}>Show Success</button>
<button onClick={() => addToast('Something went wrong.', 'error')}>Show Error</button>
<button onClick={() => addToast('Here is some info.', 'info')}>Show Info</button>
</div>
);
}
function App() {
return (
<ToastProvider>
<Inner />
</ToastProvider>
);
}
export default App;
Key Concepts
- Context + Portal: The portal lives inside a Context provider. Children anywhere in the tree can trigger toasts via
useToast(). - aria-live=”polite”: Screen readers announce new toast messages without interrupting the user.
- Auto-dismiss:
setTimeoutremoves toasts after the specified duration.
