Toast

Temporary notification that auto-dismisses.

To trigger a toast from any element, add the attribute data-component="toast" and configure it with data attributes:

  • data-toast-type="info"info, success, warning, or error
  • data-toast-position="top-right" – see Params for all positions
  • data-toast-dismissible="true" – whether a close button is shown
  • data-toast-value="Your message" – the toast text

Not dismissible

Toasts can be made non‑dismissible by setting data-toast-dismissible="false". They will disappear automatically after the duration expires and cannot be closed manually.

Warning

Use data-toast-type="warning" to display a warning notification with appropriate styling and icon.

Error

Use data-toast-type="error" for error messages. Error toasts typically have a red border and background.

Usage

Include the JavaScript code in your project:

/**
 * Inject necessary CSS animations for toast transitions
 */
const inject_toast_styles = () => {
  if (document.getElementById("toast-animation")) return;

  const style = document.createElement("style");
  style.id = "toast-animation";
  style.textContent = `
@keyframes fade-in { 0% { opacity: 0; transform: scale(0.95); } 100% { opacity: 1; transform: scale(1); } }
@keyframes fade-out { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(0.95); } }
`;
  document.head.appendChild(style);
};

// Position configuration mapping
const POSITION_CLASSES = {
  "top-left": "top-0 left-0",
  "top-center": "top-0 left-[50%] -translate-x-[50%]",
  "top-right": "top-0 right-0",
  "bottom-left": "bottom-0 left-0",
  "bottom-center": "bottom-0 left-[50%] -translate-x-[50%]",
  "bottom-right": "bottom-0 right-0"
};

// Toast type configuration
const TOAST_CONFIGS = {
  info: {
    classes: "border-black-100 dark:border-black-700 bg-background",
    text: "Notification"
  },
  success: {
    classes: "border-green-500 bg-green-50 dark:bg-green-900", 
    text: "Success"
  },
  warning: {
    classes: "border-yellow-500 bg-yellow-50 dark:bg-yellow-900",
    text: "Alert"
  },
  error: {
    classes: "border-red-500 bg-red-50 dark:bg-red-900",
    text: "Error"
  }
};

/**
 * Create toast container with specified position
 */
const create_toast_container = (position) => {
  const container = document.createElement("div");
  container.className = `fixed z-[9999] p-16px flex flex-col gap-8px pointer-none ${POSITION_CLASSES[position] || ""} toast-container`;
  container.dataset.position = position;
  return container;
};

/**
 * Get existing toast container or create new one
 */
const get_container = (position) => {
  const existing = document.querySelector(`.toast-container[data-position="${position}"]`);
  if (existing) return existing;
  
  const container = create_toast_container(position);
  document.body.appendChild(container);
  return container;
};

// Optimized SVG icons (removed duplicate clipPath definitions)
const TOAST_ICONS = {
  info: `<svg width="28" height="29" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M11.6666 7.5H8.16665C7.54781 7.5 6.95432 7.74583 6.51673 8.18342C6.07915 8.621 5.83331 9.21449 5.83331 9.83333V20.3333C5.83331 20.9522 6.07915 21.5457 6.51673 21.9832C6.95432 22.4208 7.54781 22.6667 8.16665 22.6667H18.6666C19.2855 22.6667 19.879 22.4208 20.3166 21.9832C20.7541 21.5457 21 20.9522 21 20.3333V16.8333" stroke="#ABADB8" stroke-linecap="round" stroke-linejoin="round"/>
    <path d="M19.8333 12.166C21.7663 12.166 23.3333 10.599 23.3333 8.66602C23.3333 6.73302 21.7663 5.16602 19.8333 5.16602C17.9003 5.16602 16.3333 6.73302 16.3333 8.66602C16.3333 10.599 17.9003 12.166 19.8333 12.166Z" stroke="#ABADB8" stroke-linecap="round" stroke-linejoin="round"/>
  </svg>`,

  success: `<svg width="28" height="29" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M5.83331 14.4993L11.6666 20.3327L23.3333 8.66602" stroke="#7CCF00" stroke-linecap="round" stroke-linejoin="round"/>
  </svg>`,

  warning: `<svg width="28" height="29" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M14 25C19.799 25 24.5 20.299 24.5 14.5C24.5 8.70101 19.799 4 14 4C8.20101 4 3.5 8.70101 3.5 14.5C3.5 20.299 8.20101 25 14 25Z" stroke="#F0B100" stroke-linecap="round" stroke-linejoin="round"/>
    <path d="M14 9.83398V14.5007" stroke="#F0B100" stroke-linecap="round" stroke-linejoin="round"/>
    <path d="M14 19.166H14.0117" stroke="#1D1D1F" stroke-linecap="round" stroke-linejoin="round"/>
  </svg>`,

  error: `<svg width="28" height="29" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M21 7.5L7 21.5" stroke="#EE5449" stroke-linecap="round" stroke-linejoin="round"/>
    <path d="M7 7.5L21 21.5" stroke="#EE5449" stroke-linecap="round" stroke-linejoin="round"/>
  </svg>`
};

const ANIMATION_CLASSES = {
  enter: "animate-[fade-in_.3s_ease-out]",
  exit: "animate-[fade-out_.3s_ease-out]"
};

const DEFAULT_OPTIONS = {
  type: "info",
  duration: 3000,
  position: "bottom-right",
  dismissible: true,
  max_toasts: 5,
  value: "Toast content"
};

// Reusable close button SVG
const CLOSE_ICON = `<svg width="20" height="21" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M15 5.5L5 15.5M5 5.5L15 15.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;

class Toast {
  /**
   * Generate toast DOM element with specified options
   */
  static #generate_toast_element(options) {
    const config = TOAST_CONFIGS[options.type] || TOAST_CONFIGS.info;
    const toast = document.createElement("div");

    toast.className = `flex gap-12px py-16px px-20px items-center pointer-events-auto rounded-8px border min-w-[320px] max-w-[550px] w-[80vw] text-foreground ${config.classes} shadow-[0px_6px_24px_-7px_#0000002E] toast ${ANIMATION_CLASSES.enter}`;
    toast.setAttribute("role", "alert");
    toast.setAttribute("aria-live", "polite");

    // Icon
    const icon = document.createElement("div");
    icon.className = "grid items-center justify-center";
    icon.innerHTML = TOAST_ICONS[options.type] || TOAST_ICONS.info;
    toast.appendChild(icon);

    // Text content
    const text_container = document.createElement("div");
    text_container.className = "grid ps-4px";
    
    const header = document.createElement("span");
    header.className = "text-h10";
    header.textContent = config.text;
    text_container.appendChild(header);
    
    const message = document.createElement("span");
    message.className = "text-p4";
    message.textContent = options.message;
    text_container.appendChild(message);
    
    toast.appendChild(text_container);

    // Close button
    if (options.dismissible && options.dismissible !== "false") {
      const close_btn = document.createElement("button");
      close_btn.className = "text-foreground opacity-80 hover:opacity-100 transition-opacity duration-300 ms-auto js_toast-close";
      close_btn.innerHTML = CLOSE_ICON;
      close_btn.setAttribute("aria-label", "Close notification");
      close_btn.onclick = () => this.#remove_toast(toast);
      toast.appendChild(close_btn);
    }

    return toast;
  }

  /**
   * Remove toast with fade-out animation
   */
  static #remove_toast(toast) {
    toast.classList.remove(ANIMATION_CLASSES.enter);
    toast.classList.add(ANIMATION_CLASSES.exit);

    const removeElement = () => {
      toast.remove();
      // Clean up empty container
      const container = toast.parentElement;
      if (container && !container.children.length) {
        container.remove();
      }
    };

    toast.addEventListener("animationend", removeElement, { once: true });
  }

  /**
   * Display toast with given options
   */
  static show(options) {
    const merged = { ...DEFAULT_OPTIONS, ...options };
    const container = get_container(merged.position);
    const toast = this.#generate_toast_element(merged);

    // Remove oldest toasts if exceeding limit
    const toasts = container.querySelectorAll(".toast");
    const overflow = toasts.length + 1 - merged.max_toasts;
    if (overflow > 0) {
      for (let i = 0; i < overflow; i++) {
        this.#remove_toast(toasts[i]);
      }
    }

    container.appendChild(toast);

    let timeout_id = null;
    if (merged.duration > 0) {
      timeout_id = setTimeout(() => {
        console.log(merged.duration);
        this.#remove_toast(toast);
        merged.onClose?.();
      }, merged.duration);
    }

    // Handle close button timeout cleanup
    const close_btn = toast.querySelector(".js_toast-close");
    if (close_btn && merged.dismissible && merged.dismissible !== "false") {
      const original_click = close_btn.onclick;
      close_btn.onclick = (e) => {
        if (timeout_id) clearTimeout(timeout_id);
        merged.onClose?.();
        original_click?.call(close_btn, e);
      };
    }
  }

  // Convenience methods
  static success(message, options) {
    this.show({ ...options, message, type: "success" });
  }

  static error(message, options) {
    this.show({ ...options, message, type: "error" });
  }

  static warning(message, options) {
    this.show({ ...options, message, type: "warning" });
  }

  static info(message, options) {
    this.show({ ...options, message, type: "info" });
  }

  /**
   * Remove all currently displayed toasts
   */
  static clear() {
    document.querySelectorAll(".toast").forEach(toast => {
      this.#remove_toast(toast);
    });
  }
}

// Toast type to method mapping
const TOAST_METHODS = {
  "info": Toast.info,
  "success": Toast.success, 
  "warning": Toast.warning,
  "error": Toast.error
};

/**
 * Get toast options from element dataset
 */
const get_toast_options = (element, globalMaxToasts) => ({
  type: element.dataset.toastType ?? DEFAULT_OPTIONS.type,
  duration: element.dataset.toastDuration ? +element.dataset.toastDuration : DEFAULT_OPTIONS.duration,
  position: element.dataset.toastPosition ?? DEFAULT_OPTIONS.position,
  dismissible: element.dataset.toastDismissible ?? DEFAULT_OPTIONS.dismissible,
  value: element.dataset.toastValue ?? DEFAULT_OPTIONS.value,
  max_toasts: globalMaxToasts
});

/**
 * Initialize toast functionality for data-attribute driven toasts
 */
const init_toasts = (options) => {
  inject_toast_styles();

  const { max_toasts = DEFAULT_OPTIONS.max_toasts } = options || {};

  document.querySelectorAll("[data-component='toast']").forEach(elem => {
    const toast_options = get_toast_options(elem, max_toasts);
    const toast_method = TOAST_METHODS[toast_options.type] || TOAST_METHODS.info;

    elem.addEventListener("click", () => {
      toast_method.call(Toast, toast_options.value, toast_options);
    });
  });
};

/**
 * Programmatically show a toast with specified options
 */
const show_toast = (options) => {
  inject_toast_styles();
  
  const {
    max_toasts = DEFAULT_OPTIONS.max_toasts,
    type = DEFAULT_OPTIONS.type,
    duration = DEFAULT_OPTIONS.duration,
    position = DEFAULT_OPTIONS.position,
    dismissible = DEFAULT_OPTIONS.dismissible,
    value = DEFAULT_OPTIONS.value
  } = options || {};

  const toast_method = TOAST_METHODS[type] || TOAST_METHODS.info;
  toast_method.call(Toast, value, { duration, position, dismissible, max_toasts });
};

window.init_toasts = init_toasts;
window.show_toast = show_toast;

Then initialise the toast system:

init_toasts();

You can pass an optional configuration object to init_toasts() – see init_toasts params below.

After initialisation, any element with data-component="toast" will trigger a toast on click. You can also call toasts programmatically using the show_toast() function.

Auto‑initialisation

Add this at the end of your toast.js file:

document.addEventListener("DOMContentLoaded", () => init_toasts());

Then link the script in your HTML:

<script src="toast.js"></script>

Params

init_toasts params

The init_toasts() function accepts an optional object with the following property:

init_toasts({
  max_toasts: 4,  // maximum toast counter
});

Custom JS Toast call

You can show a toast programmatically using the global show_toast() function:

show_toast({
  max_toasts: 4,            // maximum toast counter
  type: "info",             // 'info' | 'success' | 'warning' | 'error'
  duration: 5000,           // toast duration
  position: "top-center",   // 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
  dismissible: true,        // can user dismiss a toast
  value: "show_toast test"  // toast text
});

Data Attributes

All options available in show_toast() can also be set via data attributes on the trigger element (prefixed with data-toast-):

  • data-toast-type – string: 'info', 'success', 'warning', 'error' (default: 'info')
  • data-toast-duration – number: time in milliseconds before auto‑dismiss (default: 3000)
  • data-toast-position – string: 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right' (default: 'bottom-right')
  • data-toast-dismissible – boolean: 'true' or 'false' (default: 'true')
  • data-toast-value – string: the toast message (default: 'Toast content')

These attributes override any global defaults.