Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

Toast

A transient message with an optional action, shown one at a time.

A transient message that appears briefly, optionally with a single action (the undo/snackbar pattern). Toasts follow MD3 snackbar semantics — one at a time, additional toasts queue.

Installation

npx touchcn add toast

Usage

Mount the <tcn-toaster /> outlet once near your app root, then trigger toasts imperatively from anywhere.

import { TouchcnToast } from '@touchcn/angular/toast';

export class Example {
  private readonly toast = inject(TouchcnToast);

  archive() {
    this.toast.show({
      message: 'Conversation archived',
      action: { label: 'Undo', onClick: () => this.restore() },
    });
  }
}
import { toast } from '@touchcn/react';
import { TcnToaster } from '@/components/ui/toast';

// Render once near the app root:
<TcnToaster />;

// Trigger from anywhere:
toast({
  message: 'Conversation archived',
  action: { label: 'Undo', onClick: () => restore() },
});
<script setup lang="ts">
import { toast } from '@touchcn/vue';
import { TcnToaster } from '@/components/ui/toast';

// Trigger from anywhere:
function archive() {
  toast({
    message: 'Conversation archived',
    action: { label: 'Undo', onClick: () => restore() },
  });
}
</script>

<template>
  <!-- Render once near the app root: -->
  <TcnToaster />
</template>

API

Member Type Description
show / toast (options) => { dismiss(): void } Enqueue a toast; returns a dismiss handle.
message string The message text.
action { label: string; onClick?: () => void } Optional action button.
duration number Auto-dismiss delay in ms (0 disables). Default 4000.
Source · Angular
export * from './tcn-toaster';
import { Component, computed, inject } from '@angular/core';
import { TouchcnToast } from '@touchcn/angular/toast';
import type { TcnToastAction } from '@touchcn/angular/toast';

/**
 * Toast outlet — mount once near the app root (e.g. in the root component's
 * template). It renders the toast currently owned by the `TouchcnToast`
 * service; call `TouchcnToast.show({ message, action?, duration? })` from
 * anywhere to enqueue one. Queue, timers and enter/exit sequencing live in the
 * engine service; this component is markup only.
 */
@Component({
  selector: 'tcn-toaster',
  template: `
    @if (toast.current(); as current) {
      <div
        class="tcn-toast-viewport fixed inset-x-0 bottom-0 z-50 flex justify-center px-4"
        [attr.data-state]="state()"
      >
        <div class="tcn-toast" role="status" aria-live="polite" aria-atomic="true">
          <span class="tcn-toast-message">{{ current.message }}</span>
          @if (current.action; as action) {
            <button type="button" class="tcn-toast-action" (click)="onAction(action)">{{ action.label }}</button>
          }
        </div>
      </div>
    }
  `,
})
export class TcnToaster {
  protected readonly toast = inject(TouchcnToast);

  protected readonly state = computed(() => (this.toast.visible() ? 'open' : 'closed'));

  protected onAction(action: TcnToastAction): void {
    action.onClick?.();
    this.toast.dismiss();
  }
}
Source · React
export * from './tcn-toaster';
import { useToast, dismissToast } from '@touchcn/react';

/**
 * Toast outlet — render once near the app root (e.g. under `TouchcnProvider`).
 * It renders the toast currently owned by the engine store; call
 * `toast({ message, action?, duration? })` from anywhere to enqueue one. Queue,
 * timers and enter/exit sequencing live in the engine store; this component is
 * markup only.
 */
export function TcnToaster() {
  const { current, visible } = useToast();
  if (!current) {
    return null;
  }
  const action = current.action;
  return (
    <div
      className="tcn-toast-viewport fixed inset-x-0 bottom-0 z-50 flex justify-center px-4"
      data-state={visible ? 'open' : 'closed'}
    >
      <div className="tcn-toast" role="status" aria-live="polite" aria-atomic="true">
        <span className="tcn-toast-message">{current.message}</span>
        {action && (
          <button
            type="button"
            className="tcn-toast-action"
            onClick={() => {
              action.onClick?.();
              dismissToast();
            }}
          >
            {action.label}
          </button>
        )}
      </div>
    </div>
  );
}
Source · Vue
<script setup lang="ts">
import { dismissToast, useToast } from '@touchcn/vue';

/**
 * Toast outlet — render once near the app root (e.g. under the component that
 * called `provideTouchcn`). It renders the toast currently owned by the engine
 * store; call `toast({ message, action?, duration? })` from anywhere to enqueue
 * one. Queue, timers and enter/exit sequencing live in the engine store; this
 * component is markup only.
 */
const state = useToast();

const onAction = (): void => {
  state.value.current?.action?.onClick?.();
  dismissToast();
};
</script>

<template>
  <div
    v-if="state.current"
    class="tcn-toast-viewport fixed inset-x-0 bottom-0 z-50 flex justify-center px-4"
    :data-state="state.visible ? 'open' : 'closed'"
  >
    <div class="tcn-toast" role="status" aria-live="polite" aria-atomic="true">
      <span class="tcn-toast-message">{{ state.current.message }}</span>
      <button v-if="state.current.action" type="button" class="tcn-toast-action" @click="onAction">
        {{ state.current.action.label }}
      </button>
    </div>
  </div>
</template>
export { default as TcnToaster } from './TcnToaster.vue';

Last updated on July 24, 2026

Was this page helpful?