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';