Toast notifications with Stimulus and Tailwind

Jordan Lee Jan 2026
3 tabs
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')
  }
}
3 files · javascript, erb Explain with highlit

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.