Toaster 轻提示
在视口一角弹出短暂的通知(保存成功、复制完成、操作失败),几秒后自动消失。行为照源包依赖的 sonner:最新的在最前,后面的缩小叠在下面;悬停、聚焦或页面切到后台时暂停计时并展开; 按住往边缘滑动可以关掉一条。需要用户确认的事用 Modal,页面上常驻的提示用 Alert。
整个应用挂一个 toaster(通常放在根组件末尾),通知由 toast() 或元素的 show() 弹出。它本身不占版面,没有通知时页面上什么也看不到。
页面上没有通知时 toaster 不可见;下面的代码就是它的全部写法。
<pt-toaster position="bottom-right" close-button></pt-toaster>用法
整个应用挂一个 <pt-toaster>,然后在任何地方调 toast():
import { toast } from '@ptengine/ui/toast';
toast('已复制到剪贴板');
toast.success('保存成功', { description: '改动已同步到所有成员' });
toast.error('保存失败');
toast.warning('配额即将用完');
toast.info('有新版本可用');
const id = toast.loading('上传中…');
toast.success('上传完成', { id }); // 同一个 id:原地更新那一条
toast.dismiss(id); // 不传 id 关掉全部三个框架都从 @ptengine/ui/toast 导入,包装包里不再导出一份。它不是组件,也不持有状态: 通知全在页面上的 <pt-toaster> 里,toast() 只是找到它(没有就在 <body> 末尾建一个)并调它的方法。 所以不挂 toaster 也能用 —— 挂了的话位置、rich-colors 这些由你挂的那个决定。
import { PtToaster } from '@ptengine/ui-react';
import { toast } from '@ptengine/ui/toast';
export function App() {
return (
<>
<button onClick={() => toast.success('已保存')}>保存</button>
<PtToaster position="top-center" richColors />
</>
);
}<script setup lang="ts">
import { PtToaster } from '@ptengine/ui-vue';
import { toast } from '@ptengine/ui/toast';
</script>
<template>
<button @click="toast.success('已保存')">保存</button>
<PtToaster position="top-center" rich-colors />
</template>import { Component } from '@angular/core';
import { PtToaster } from '@ptengine/ui-angular';
import { toast } from '@ptengine/ui/toast';
@Component({
selector: 'app-root',
standalone: true,
imports: [PtToaster],
template: `
<button (click)="save()">保存</button>
<pt-toaster position="top-center" rich-colors></pt-toaster>
`
})
export class AppComponent {
save() {
toast.success('已保存');
}
}Angular 里注意:action.onClick / onDismiss / onAutoClose 由 toaster 自己的点击与计时触发,包装层又让组件在 NgZone 之外渲染,这些回调因此跑在 zone 外 —— 里面改组件状态要包一层 ngZone.run(() => …),否则界面不会立刻刷新。
调用是异步交付的(下一个宏任务):首屏渲染过程中调用时,框架还来不及把 <PtToaster> 挂进 DOM, 等一拍就不会先建出一个多余的 toaster。返回的 id 是同步给的。服务端渲染(Node 里没有 document)时 所有调用都是空操作,只返回 id,可以放心在 SSR 会走到的代码里调用。
也可以不经 toast(),直接调元素的方法:show(options) 返回 id;update(id, options) 只改给出的几项; dismiss(id?)。
选项
toast(message, options) 的第一个参数是标题;show(options) 里标题写在 title 上。
| 选项 | 说明 |
|---|---|
id | 不传时自动分配递增数字;与屏幕上某条相同时改为更新那一条 |
type | default / success / error / warning / info / loading(toast.success() 这些快捷方法替你填) |
description | 标题下的说明 |
action | { label, onClick }:主操作按钮,点了关闭这条;onClick 里 event.preventDefault() 就不关 |
cancel | { label, onClick }:次要按钮,点了关闭这条 |
duration | 停留毫秒数,默认用 toaster 的 duration(4000);0 或 Infinity 不自动关闭;loading 始终不自动关 |
dismissible | 默认 true;false 时不显示关闭按钮、不能滑走。action / cancel 照常执行并关闭,dismiss() 与到时照常关闭 |
closeButton | 这一条是否显示关闭按钮,不给时跟随 toaster 的 close-button |
richColors | 这一条是否用语义底色,不给时跟随 toaster 的 rich-colors |
onDismiss | 被关闭按钮、滑动或 dismiss() 关掉时调用(点 action / cancel 关掉的不调) |
onAutoClose | 计时到了自动关闭时调用 |
内容只收纯文本 —— 标题、说明、按钮文案,不收插槽或 HTML。
duration: 0 与 sonner 不同(sonner 把 0 当成「没给」):这里沿用 AntD message 的约定,0 表示常驻。
promise
toast.promise(saveReport(), {
loading: '保存中…',
success: report => `已保存「${report.name}」`,
error: error => `保存失败:${(error as Error).message}`
});先弹一条 loading,兑现后原地换成 success,拒绝后换成 error;对应阶段没给文案时直接关掉那条 loading。 传进去的 Promise 照常 await,这里不吞它的结果。
位置、堆叠与层级
position 取 top-left / top-center / top-right / bottom-left / bottom-center / bottom-right(默认,与 sonner 一致), 是物理方向,RTL 下不翻转;offset 是离视口边缘的距离(默认 24px)。窄屏(≤600px)下铺满宽度、左右各留 16px。
默认最多露出 3 条(visible-toasts),后面的每条缩小 5%、错开一个间距叠在下面;悬停或键盘聚焦时展开, 条与条之间隔 gap(默认 14px)。expand 让它始终展开。
通知区经 popover API 进顶层,不被祖先的 overflow / transform 影响。顶层按进入先后叠放: 通知出现之后才打开的弹窗会盖住它;弹窗打开期间出新通知时,通知区会重新提到最上面,按钮照样点得到,弹窗也不会被关掉。 宿主带 aria-live,模态气泡卡片把页面其余部分设为 inert 时会跳过它 —— 前提是 toaster 与弹层所在的那条祖先链是兄弟关系, 所以最好挂在应用根组件的末尾(或者干脆不挂,让 toast() 建在 <body> 末尾)。
视觉
白底(--pt-bg-box)、1px 描边、shadow-lg、圆角 8px、宽 356px、内边距 16px;标题 13/20 500 strong 色,说明 13/20 standard 色。 success / error / warning / info 只有图标染色,文字仍是中性色(源包的取舍:颜色已经由图标表达,彩色文字更难读)。 rich-colors 再把整条换成对应的语义浅底与描边,文字照样不染。action 按钮是高强调黑,cancel 按钮是次要灰。 系统开了「减少动态效果」时不播入场、堆叠与退场过渡,loading 的转圈也停。
键盘与读屏
- Alt+T:焦点移到通知区并展开(同 sonner),Tab 在通知与按钮之间移动 (弹窗打开时也一样,走到头回到弹窗里); Esc 收起,焦点还给按快捷键之前的元素。弹窗打开时这次 Esc 只收起通知区,不会关弹窗。
- 宿主是
aria-live="polite"的播报区,每条通知role="status",error那条aria-live="assertive"立即打断播报。 - 通知区的读屏名称默认「通知 Alt+T」(
t('toast.label')),label可以改;关闭按钮默认「关闭通知」(t('toast.close')),close-label可以改。
API
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
closeButton | close-button | boolean | false | 每条通知的起始角显示关闭按钮。每条可用 `closeButton` 单独覆盖;loading 类型不显示 |
closeLabel | close-label | string | undefined | — | 关闭按钮的无障碍名称。不设置时用内置文案(`t('toast.close')`,随 locale 切换) |
duration | duration | number | 4000 | 默认停留时长(ms)。每条的 `duration` 优先;`0` 表示都不自动关闭 |
expand | expand | boolean | false | 始终展开(不堆叠),每条之间隔 `gap` |
gap | gap | number | 14 | 展开时通知之间的间距(px) |
label | label | string | undefined | — | 通知区的读屏名称,后面会接上「Alt+T」。不设置时用内置文案(`t('toast.label')`) |
offset | offset | number | string | undefined | — | 离视口边缘的距离:数字按 px,也可以写任意 CSS 长度。默认 24px(窄屏 16px,不受它影响) |
position | position | "bottom-center" | "bottom-left" | "bottom-right" | "top-center" | "top-left" | "top-right" | 'bottom-right' | 在视口的哪个角(或上下边的中间) |
richColors | rich-colors | boolean | false | 用语义底色:success / error / warning / info 的整条通知换成对应的浅底与描边(文字仍是中性色) |
visibleToasts | visible-toasts | number | 3 | 堆叠时最多露出几条,更早的淡出(仍在计时) |
方法
| 方法 | 说明 |
|---|---|
dismiss(id?: ToastId) => Promise<void> | 关闭一条通知(传 id)或全部(不传)。会调用对应通知的 `onDismiss` |
show(options: ToastOptions) => Promise<ToastId> | 显示一条通知,返回它的 id。`options.id` 与屏幕上某条相同时改为更新那一条(等同 `update()`)。 各项含义见 `ToastOptions`;回调(按钮的 `onClick`、`onDismiss`、`onAutoClose`)只能经 JS 传。 |
update(id: ToastId, options: ToastOptions) => Promise<void> | 更新屏幕上的一条通知(只改给出的几项),并重新开始计时。那条已经关闭时什么也不做 |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
action | 主操作按钮 |
cancel | 次要按钮 |
close | 关闭按钮(`close-button` 开启时) |
description | 说明 |
icon | 类型图标(default 类型没有) |
list | 通知区(进顶层的那一层,带读屏名称) |
title | 标题 |
toast | 每一条通知 |
CSS 变量
| 变量 | 说明 |
|---|---|
--pt-toaster-radius | 通知圆角,默认 --pt-radius-md(8px) |
--pt-toaster-width | 通知宽度,默认 356px(窄屏下铺满,不受它影响) |