Toast

Transient notifications that stack and auto-dismiss. Each toast schedules a timer to remove itself; the timers are tracked per id and cleared in onDestroy so nothing leaks. The list renders through data-each.

Markup

<button @click="success">Show success</button>
<button @click="error">Show error</button>

<div class="toast-stack" aria-live="polite">
  <template data-each="toasts" data-key="id">
    <div data-bind="class:toastClass(item.kind)">
      <span data-text="item.msg"></span>
      <button @click="dismiss" data-bind="data-id:item.id" aria-label="Dismiss">
        &times;
      </button>
    </div>
  </template>
</div>

Component

Micra.define("toast", {
  state: { toasts: [] },
  success() {
    this.push("success", "Changes saved");
  },
  error() {
    this.push("error", "Something went wrong");
  },
  push(kind, msg) {
    this._n = (this._n || 0) + 1;
    const id = this._n;
    this.state.toasts = [...this.state.toasts, { id, kind, msg }];
    this._timers = this._timers || {};
    this._timers[id] = setTimeout(() => this.dismissId(id), 3200);
  },
  dismiss(e) {
    this.dismissId(Number(e.currentTarget.dataset.id));
  },
  dismissId(id) {
    clearTimeout(this._timers?.[id]);
    this.state.toasts = this.state.toasts.filter((t) => t.id !== id);
  },
  toastClass(kind) {
    return kind === "success" ? "toast toast-success" : "toast toast-error";
  },
  onDestroy() {
    for (const id in this._timers || {}) clearTimeout(this._timers[id]);
  },
});

Pushing a toast replaces the toasts array (not a mutation), so the keyed data-each adds exactly one row. Removal filters by id and clears that toast’s timer.