import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["container"]
connect() {
// Make toast controller globally available
window.toast = this
}
show(message, type = 'info', duration = 5000) {
const toast = this.createToast(message, type)
this.containerTarget.appendChild(toast)
// Trigger animation
requestAnimationFrame(() => {
toast.classList.remove('translate-x-full', 'opacity-0')
})
// Auto dismiss
if (duration > 0) {
setTimeout(() => {
this.dismiss(toast)
}, duration)
}
}
createToast(message, type) {
const toast = document.createElement('div')
toast.className = `toast toast-${type} flex items-center gap-3 p-4 rounded-lg shadow-lg mb-4 transform translate-x-full opacity-0 transition-all duration-300`
const icons = {
success: '<i class="fas fa-check-circle text-green-500"></i>',
error: '<i class="fas fa-exclamation-circle text-red-500"></i>',
warning: '<i class="fas fa-exclamation-triangle text-yellow-500"></i>',
info: '<i class="fas fa-info-circle text-blue-500"></i>'
}
const colors = {
success: 'bg-green-50 border-green-200',
error: 'bg-red-50 border-red-200',
warning: 'bg-yellow-50 border-yellow-200',
info: 'bg-blue-50 border-blue-200'
}
toast.classList.add(colors[type] || colors.info)
toast.innerHTML = `
${icons[type] || icons.info}
<p class="flex-1">${message}</p>
<button type="button" class="text-gray-400 hover:text-gray-600" data-action="toast#dismissTarget">
<i class="fas fa-times"></i>
</button>
`
return toast
}
dismissTarget(event) {
const toast = event.target.closest('.toast')
this.dismiss(toast)
}
dismiss(toast) {
toast.classList.add('translate-x-full', 'opacity-0')
setTimeout(() => {
toast.remove()
}, 300)
}
success(message) {
this.show(message, 'success')
}
error(message) {
this.show(message, 'error')
}
warning(message) {
this.show(message, 'warning')
}
info(message) {
this.show(message, 'info')
}
}
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
<!-- Toast notification container -->
<div data-controller="toast"
aria-live="polite"
aria-atomic="true"
class="fixed top-4 right-4 z-50 max-w-md">
<div data-toast-target="container"></div>
</div>
</body>
</html>
// From any Stimulus controller or script
window.toast.success('Post created successfully!')
window.toast.error('Failed to save post')
window.toast.info('Draft saved')
window.toast.warning('Connection unstable')
Toast notifications provide non-intrusive feedback for user actions. I build a notification system with Stimulus that manages a stack of toasts, auto-dismisses them after a timeout, and supports manual dismissal. Notifications are queued when multiple fire simultaneously, with smooth enter/exit animations. Each toast has a type (success, error, info) that determines its color and icon. The controller exposes a global method that other controllers can call to show notifications. I also integrate with Turbo Stream responses so server actions can trigger toasts. The notification container is positioned fixed and stacks toasts vertically. Accessibility is maintained with ARIA live regions that announce messages to screen readers.