Toast 轻提示

操作完成后浮出的一条简短反馈。

'use client'

import { Button, Inline, toast } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline align="center" className="gap-3">
      <Button variant="outline" tone="neutral" onClick={() => toast('草稿已保存')}>
        默认
      </Button>
      <Button variant="outline" tone="neutral" onClick={() => toast.success('文章已发布')}>
        成功
      </Button>
      <Button
        variant="outline"
        tone="neutral"
        onClick={() => toast.danger('上传失败', { description: '文件超过 20 MB。' })}
      >
        失败
      </Button>
    </Inline>
  )
}
tsx

用法

import { Toaster, toast } from '@hina-ui/react'
ts

在应用的最外层挂载一个 Toaster,然后在任何地方调用 toast()。调用后返回这条提示的 id,可以用它更新或关闭这条提示。

'use client'

import { Button, toast } from '@hina-ui/react'

export default function Demo() {
  return (
    <Button variant="outline" tone="neutral" onClick={() => toast('草稿已保存')}>
      保存草稿
    </Button>
  )
}
tsx
import type { ReactNode } from 'react'
import { AppShell, Toaster } from '@hina-ui/react'

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        <AppShell>
          {children}
          <Toaster />
        </AppShell>
      </body>
    </html>
  )
}
tsx

示例

语义

五个语义各有自己的图标和颜色。loading 显示转圈图标,并且一直停留,通常再用同一个 id 把它更新为最终结果。

'use client'

import { Button, Inline, toast } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline align="center" className="gap-3">
      <Button variant="outline" tone="neutral" onClick={() => toast.success('已加入书架')}>
        success
      </Button>
      <Button variant="outline" tone="neutral" onClick={() => toast.danger('网络连接中断')}>
        danger
      </Button>
      <Button variant="outline" tone="neutral" onClick={() => toast.warning('还有未保存的改动')}>
        warning
      </Button>
      <Button variant="outline" tone="neutral" onClick={() => toast.info('新版本已经可用')}>
        info
      </Button>
      <Button variant="outline" tone="neutral" onClick={() => toast.loading('正在同步')}>
        loading
      </Button>
    </Inline>
  )
}
tsx

说明文字

description 是消息下面的第二行,用来补充细节。

'use client'

import { Button, toast } from '@hina-ui/react'

export default function Demo() {
  return (
    <Button
      variant="outline"
      tone="neutral"
      onClick={() =>
        toast.success('导入完成', {
          description: '共导入 128 本书,其中 3 本因格式不符被跳过。',
        })
      }
    >
      带说明的提示
    </Button>
  )
}
tsx

操作按钮

action 是主按钮,cancel 是次按钮,点击其中任何一个都会关闭这条提示。需要用户回应时,把 duration 设为 0。

'use client'

import { Button, Inline, toast } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline align="center" className="gap-3">
      <Button
        variant="outline"
        tone="neutral"
        onClick={() =>
          toast('已移入回收站', {
            action: { label: '撤销', onClick: () => toast.success('已还原') },
          })
        }
      >
        带操作
      </Button>
      <Button
        variant="outline"
        tone="neutral"
        onClick={() =>
          toast.warning('确定要清空吗', {
            duration: 0,
            action: { label: '清空', onClick: () => toast.success('已清空') },
            cancel: { label: '取消' },
          })
        }
      >
        两个按钮
      </Button>
    </Inline>
  )
}
tsx

异步任务

toast.promise 接收一个 Promise,自行在 loading、success 和 error 三种状态之间切换。success 和 error 可以写成函数,用结果拼出消息。

'use client'

import { Button, toast } from '@hina-ui/react'

function upload() {
  const task = new Promise<{ name: string }>(resolve =>
    setTimeout(() => resolve({ name: 'ATRI.epub' }), 2000),
  )
  toast.promise(task, {
    loading: '正在上传',
    success: book => `${book.name} 上传完成`,
    error: '上传失败,请重试',
  })
}

export default function Demo() {
  return (
    <Button variant="outline" tone="neutral" onClick={upload}>
      上传文件
    </Button>
  )
}
tsx

原地更新

传入相同的 id 时,更新已有提示的语义、文字和计时,而不是新增一条。进度类的反馈就是这样实现的。

'use client'

import { Button, toast } from '@hina-ui/react'

function sync() {
  toast.loading('正在同步', { id: 'sync' })
  setTimeout(() => toast.loading('已同步 12 / 30', { id: 'sync' }), 1000)
  setTimeout(() => toast.success('同步完成', { id: 'sync' }), 2000)
}

export default function Demo() {
  return (
    <Button variant="outline" tone="neutral" onClick={sync}>
      开始同步
    </Button>
  )
}
tsx

自定义渲染

toast.custom 用自己的组件渲染提示的正文。props 里的内容原样传给它,组件还会收到一个 toastId。卡片的底色、边框、阴影和内边距仍然由 Toaster 提供。

'use client'

import { BookOpen } from 'lucide-react'
import { Button, Heading, Inline, Stack, Text, toast } from '@hina-ui/react'

function ChapterToast({ title, chapter }: { title: string; chapter: string }) {
  return (
    <Inline align="center" className="gap-3">
      <div className="bg-inset grid size-10 shrink-0 place-items-center rounded-md">
        <BookOpen className="size-5" />
      </div>
      <Stack gap="none" className="min-w-0 flex-1">
        <Heading level={4} size="sm">
          {title}
        </Heading>
        <Text tone="muted" size="sm">
          {chapter}
        </Text>
      </Stack>
      <Button size="sm" onClick={() => toast.dismiss('chapter')}>
        去阅读
      </Button>
    </Inline>
  )
}

function notify() {
  toast.custom(ChapterToast, {
    id: 'chapter',
    duration: 6000,
    props: { title: 'ATRI', chapter: '第 42 话' },
  })
}

export default function Demo() {
  return (
    <Button variant="outline" tone="neutral" onClick={notify}>
      新章节上线
    </Button>
  )
}
tsx

停留时长

duration 的单位是毫秒,默认为 4000,loading 默认为 0。为 0 表示一直停留,直到用户点击关闭按钮,或者调用 toast.dismiss(id)。

'use client'

import { Button, Inline, toast } from '@hina-ui/react'

export default function Demo() {
  return (
    <Inline align="center" className="gap-3">
      <Button
        variant="outline"
        tone="neutral"
        onClick={() => toast('一秒后消失', { duration: 1000 })}
      >
        一秒
      </Button>
      <Button
        variant="outline"
        tone="neutral"
        onClick={() => toast.info('需要手动关闭', { id: 'sticky', duration: 0 })}
      >
        不自动关闭
      </Button>
      <Button variant="soft" tone="neutral" onClick={() => toast.dismiss('sticky')}>
        关闭上一条
      </Button>
    </Inline>
  )
}
tsx

行为

  • 指针悬停在提示区域上,或者焦点进入其中时,计时暂停,离开后继续。
  • 同时最多显示 5 条提示,更早的会退场。
  • 提示按 id 索引,用同一个 id 再次调用是更新,而不是新增。
  • 向右滑动可以关闭一条提示。

无障碍

  • 提示区域是 role="region",默认名称为“通知”,可以用 label 覆盖。
  • 每条提示通过一个独立的 role="alert" 实时区域播报,屏幕阅读器会读出消息和说明文字。
  • 最上面那条提示可以用 Tab 聚焦,关闭按钮带有无障碍名称。

API

toast

方法
说明
toast(message, options?)
中性提示,返回这条提示的 id
toast.success(...)
成功
toast.danger(...)
失败
toast.warning(...)
警告
toast.info(...)
信息
toast.loading(...)
进行中,一直停留
toast.promise(p, messages)
跟随 Promise 的三种状态
toast.custom(component, {})
用自己的组件渲染整条提示
toast.dismiss(id?)
关闭指定的一条,不传时关闭全部

ToastOptions

属性
类型
默认值
说明
id
number | string
—
相同的 id 会更新已有提示
description
string
—
消息下面的第二行
duration
number
4000
停留毫秒数,0 表示一直停留
action
{ label, onClick }
—
主按钮
cancel
{ label, onClick }
—
次按钮
onDismiss
(id) => void
—
提示被关闭时调用
onAutoClose
(id) => void
—
计时结束自动关闭时调用

Toaster

属性
类型
默认值
说明
position
ToasterPosition
—
不设置时窄屏在底部居中,宽屏在右上角
label
string
—
提示区域的无障碍名称
className
string
—
追加到提示区域上的类名

ToasterPosition 的取值为 top-start、top-center、top-end、bottom-start、bottom-center 和 bottom-end。