Skip to main content
โšก Calmops

Toast Notifications: Design and Implementation Guide

Toast notifications are brief, non-blocking messages that provide feedback to users. They’re essential for showing success messages, errors, warnings, and other information without interrupting the user’s workflow.

What are Toast Notifications?

Toasts are lightweight messages that:

  • Appear temporarily (auto-dismiss)
  • Don’t require user action
  • Provide feedback on completed actions
  • Appear in a non-intrusive location
<div class="toast" role="alert">
  <div class="toast-icon">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
  </div>
  <div class="toast-content">
    <p class="toast-message">Changes saved successfully</p>
  </div>
  <button class="toast-close" aria-label="Close">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <path d="M6 18L18 6M6 6l12 12" />
    </svg>
  </button>
</div>

Toast Types

Success Toast

.toast-success {
  background: #ecfdf5;
  border: 1px solid #a7f3d0;
  color: #065f46;
}

.toast-success .toast-icon {
  color: #10b981;
}

Error Toast

.toast-error {
  background: #fef2f2;
  border: 1px solid #fecaca;
  color: #991b1b;
}

.toast-error .toast-icon {
  color: #ef4444;
}

Warning Toast

.toast-warning {
  background: #fffbeb;
  border: 1px solid #fde68a;
  color: #92400e;
}

.toast-warning .toast-icon {
  color: #f59e0b;
}

Info Toast

.toast-info {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  color: #1e40af;
}

.toast-info .toast-icon {
  color: #3b82f6;
}

Complete CSS Implementation

/* Toast Container */
.toast-container {
  position: fixed;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem;
  max-width: 400px;
  pointer-events: none;
}

/* Position: Bottom Right (default) */
.toast-container.bottom-right {
  bottom: 0;
  right: 0;
}

/* Position: Bottom Left */
.toast-container.bottom-left {
  bottom: 0;
  left: 0;
}

/* Position: Top Right */
.toast-container.top-right {
  top: 0;
  right: 0;
}

/* Position: Top Left */
.toast-container.top-left {
  top: 0;
  left: 0;
}

/* Position: Top Center */
.toast-container.top-center {
  top: 0;
  left: 50%;
  transform: translateX(-50%);
}

/* Position: Bottom Center */
.toast-container.bottom-center {
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}

/* Toast Base */
.toast {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 1rem;
  border-radius: 8px;
  border: 1px solid;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  pointer-events: auto;
  opacity: 0;
  transform: translateX(100%);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

/* Visible state */
.toast.show {
  opacity: 1;
  transform: translateX(0);
}

/* Slide in from right */
.toast-container.bottom-right .toast,
.toast-container.top-right .toast {
  transform: translateX(100%);
}

.toast-container.bottom-right .toast.show,
.toast-container.top-right .toast.show {
  transform: translateX(0);
}

/* Slide in from left */
.toast-container.bottom-left .toast,
.toast-container.top-left .toast {
  transform: translateX(-100%);
}

/* Slide in from top */
.toast-container.top-center .toast {
  transform: translateY(-100%);
}

/* Slide in from bottom */
.toast-container.bottom-center .toast,
.toast-container.bottom-right .toast,
.toast-container.bottom-left .toast {
  transform: translateY(100%);
}

/* Icon */
.toast-icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}

.toast-icon svg {
  width: 100%;
  height: 100%;
}

/* Content */
.toast-content {
  flex: 1;
  min-width: 0;
}

.toast-message {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.4;
}

.toast-message a {
  color: inherit;
  font-weight: 600;
}

/* Optional title */
.toast-title {
  font-weight: 600;
  margin-bottom: 0.25rem;
}

/* Close button */
.toast-close {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.2s;
}

.toast-close:hover {
  opacity: 1;
}

.toast-close svg {
  width: 100%;
  height: 100%;
}

/* Duration indicator (progress bar) */
.toast-progress {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 3px;
  background: currentColor;
  opacity: 0.3;
  animation: progress linear forwards;
}

@keyframes progress {
  from { width: 100%; }
  to { width: 0%; }
}

/* With progress */
.toast.with-progress {
  position: relative;
  overflow: hidden;
}

JavaScript Implementation

class Toast {
  constructor(options = {}) {
    this.options = {
      duration: 5000,
      position: 'bottom-right',
      dismissible: true,
      ...options
    };
    
    this.createContainer();
    this.show = this.show.bind(this);
  }
  
  createContainer() {
    let container = document.querySelector(`.toast-container.${this.options.position}`);
    
    if (!container) {
      container = document.createElement('div');
      container.className = `toast-container ${this.options.position}`;
      document.body.appendChild(container);
    }
    
    this.container = container;
  }
  
  show(message, options = {}) {
    const toast = document.createElement('div');
    toast.className = `toast toast-${options.type || 'info'}`;
    
    if (this.options.dismissible) {
      toast.innerHTML = `
        <div class="toast-icon">
          ${this.getIcon(options.type || 'info')}
        </div>
        <div class="toast-content">
          <p class="toast-message">${message}</p>
        </div>
        <button class="toast-close" aria-label="Close">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      `;
      
      toast.querySelector('.toast-close').addEventListener('click', () => {
        this.dismiss(toast);
      });
    } else {
      toast.innerHTML = `
        <div class="toast-icon">
          ${this.getIcon(options.type || 'info')}
        </div>
        <div class="toast-content">
          <p class="toast-message">${message}</p>
        </div>
      `;
    }
    
    this.container.appendChild(toast);
    
    // Trigger animation
    requestAnimationFrame(() => {
      toast.classList.add('show');
    });
    
    // Auto dismiss
    const duration = options.duration || this.options.duration;
    if (duration > 0) {
      setTimeout(() => this.dismiss(toast), duration);
    }
    
    return toast;
  }
  
  dismiss(toast) {
    toast.classList.remove('show');
    setTimeout(() => toast.remove(), 300);
  }
  
  getIcon(type) {
    const icons = {
      success: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>`,
      error: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>`,
      warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
      </svg>`,
      info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>`
    };
    return icons[type] || icons.info;
  }
  
  // Convenience methods
  success(message, options = {}) {
    return this.show(message, { ...options, type: 'success' });
  }
  
  error(message, options = {}) {
    return this.show(message, { ...options, type: 'error' });
  }
  
  warning(message, options = {}) {
    return this.show(message, { ...options, type: 'warning' });
  }
  
  info(message, options = {}) {
    return this.show(message, { ...options, type: 'info' });
  }
}

// Create global instance
const toast = new Toast({
  duration: 4000,
  position: 'bottom-right'
});

// Usage
toast.success('Settings saved successfully');
toast.error('Failed to delete item. Please try again.');
toast.warning('Your session will expire in 5 minutes.');
toast.info('New version available. Refresh to update.');

React Implementation

import { createContext, useContext, useState, useCallback } from 'react';

const ToastContext = createContext(null);

export function ToastProvider({ children }) {
  const [toasts, setToasts] = useState([]);
  
  const addToast = useCallback((message, options = {}) => {
    const id = Date.now();
    setToasts(prev => [...prev, { id, message, ...options }]);
    
    setTimeout(() => {
      setToasts(prev => prev.filter(t => t.id !== id));
    }, options.duration || 4000);
  }, []);
  
  const toast = {
    success: (msg, opts) => addToast(msg, { type: 'success', ...opts }),
    error: (msg, opts) => addToast(msg, { type: 'error', ...opts }),
    warning: (msg, opts) => addToast(msg, { type: 'warning', ...opts }),
    info: (msg, opts) => addToast(msg, { type: 'info', ...opts }),
  };
  
  return (
    <ToastContext.Provider value={toast}>
      {children}
      <ToastContainer toasts={toasts} />
    </ToastContext.Provider>
  );
}

export function useToast() {
  return useContext(ToastContext);
}

function ToastContainer({ toasts }) {
  return (
    <div className="toast-container bottom-right">
      {toasts.map(toast => (
        <Toast key={toast.id} {...toast} />
      ))}
    </div>
  );
}

Accessibility

/* ARIA role */
.toast[role="alert"] {
  /* Ensures screen readers announce */
}

/* Focus visible */
.toast-close:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: opacity 0.1s ease;
    transform: none !important;
  }
}
// Set ARIA live region appropriately
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'polite');

// For errors that need immediate attention
errorToast.setAttribute('aria-live', 'assertive');

Mobile Considerations

@media (max-width: 640px) {
  .toast-container {
    left: 1rem;
    right: 1rem;
    max-width: none;
    padding: 0.5rem;
  }
  
  .toast {
    width: 100%;
  }
}

Best Practices

Do

  • Keep it brief - 1-2 sentences max
  • Use appropriate types - Success, error, warning, info
  • Good placement - Usually bottom-right or top-right
  • Auto-dismiss - 3-5 seconds is typical
  • Allow dismissal - Always include close button

Don’t

  • Don’t use for critical errors - Use modal dialogs instead
  • Don’t stack too many - Limit to 3 visible
  • Don’t block interactions - Shouldn’t prevent clicking
  • Don’t repeat actions - Avoid “Try again” in toasts
  • Don’t use for form errors - Show inline near fields

Summary

Toast notifications provide non-blocking feedback:

  • Types: success, error, warning, info
  • Placement: Usually bottom-right or top-right
  • Duration: 3-5 seconds typical
  • Animation: Slide in/out for polish
  • Accessibility: Use role=“alert”, aria-live
  • Mobile: Full width on small screens

Implement toasts thoughtfully - they’re one of the most common UI elements users encounter!

Comments