wui
组件

警告提示 Alert

用于在页面流中展示需要持久保留的状态通告、错误排查指引与操作建议。

第三方依赖 · lucide-react第三方依赖 · class-variance-authority第三方依赖 · motion

基础用法

警告提示的基础形态,包含 info、success、warning、destructive 语义变体,支持标题与详细描述:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/alert
安装依赖库与图标库
pnpm add motion lucide-react class-variance-authority clsx tailwind-merge
复制组件源码到 components/ui/alert.tsx
components/ui/alert.tsx
"use client"

import * as React from "react"
import {
  CircleCheckIcon,
  CircleXIcon,
  InfoIcon,
  TriangleAlertIcon,
  XIcon,
} from "lucide-react"
import { cva } from "class-variance-authority"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"

export type AlertVariant =
  | "default"
  | "info"
  | "success"
  | "warning"
  | "destructive"

const alertVariants = cva(
  "relative flex w-full items-start gap-3 rounded-md border px-4 py-3 text-sm",
  {
    variants: {
      variant: {
        default: "border-border bg-background text-foreground",
        info: "border-info-border bg-info-subtle text-foreground",
        success: "border-success-border bg-success-subtle text-foreground",
        warning: "border-warning-border bg-warning-subtle text-foreground",
        destructive:
          "border-destructive-border bg-destructive-subtle text-foreground",
      },
      size: {
        default: "min-h-12",
        compact: "min-h-10 px-3 py-2",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

const defaultIcons = {
  default: InfoIcon,
  info: InfoIcon,
  success: CircleCheckIcon,
  warning: TriangleAlertIcon,
  destructive: CircleXIcon,
} as const

export interface AlertProps extends Omit<React.ComponentProps<"div">, "title"> {
  /** Semantic appearance and default icon. @default "default" */
  variant?: AlertVariant
  /** Density preset. @default "default" */
  size?: "default" | "compact"
  /** Optional heading displayed above the description. */
  title?: React.ReactNode
  /** Custom leading icon. Pass `false` to hide the icon. */
  icon?: React.ReactNode | false
  /** Optional action rendered at the end of the alert. */
  action?: React.ReactNode
  /** Show a close control. @default false */
  closable?: boolean
  /** Controlled visibility. */
  visible?: boolean
  /** Initial visibility in uncontrolled mode. @default true */
  defaultVisible?: boolean
  /** Called whenever visibility changes. */
  onVisibleChange?: (visible: boolean) => void
}

/** A persistent inline status alert with optional action and dismissal. */
function Alert({
  className,
  variant = "default",
  size = "default",
  title,
  children,
  icon,
  action,
  closable = false,
  visible,
  defaultVisible = true,
  onVisibleChange,
  role,
  ...props
}: AlertProps) {
  const [internalVisible, setInternalVisible] = React.useState(defaultVisible)
  const isVisible = visible ?? internalVisible
  const DefaultIcon = defaultIcons[variant]
  const reduceMotion = useReducedMotion()

  function setVisible(next: boolean) {
    if (visible === undefined) setInternalVisible(next)
    onVisibleChange?.(next)
  }

  // The wrapper fades the alert and collapses its height, so content below
  // closes the gap smoothly instead of jumping when the alert is dismissed.
  return (
    <AnimatePresence initial={false}>
      {isVisible ? (
        <motion.div
          key="alert"
          data-slot="alert-presence"
          className="w-full"
          initial={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}
          animate={reduceMotion ? { opacity: 1 } : { height: "auto", opacity: 1 }}
          exit={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}
          transition={
            reduceMotion
              ? { duration: 0 }
              : {
                  height: { duration: 0.28, ease: [0.22, 1, 0.36, 1] },
                  opacity: { duration: 0.18, ease: "easeOut" },
                }
          }
          style={{ overflow: "hidden" }}
        >
          <div
            data-slot="alert"
            data-variant={variant}
            data-size={size}
            role={
              role ??
              (variant === "warning" || variant === "destructive"
                ? "alert"
                : "status")
            }
            className={cn(alertVariants({ variant, size }), className)}
            {...props}
          >
            {icon !== false ? (
              <span
                data-slot="alert-icon"
                className={cn(
                  "mt-0.5 flex shrink-0 text-muted-foreground [&_svg]:size-4",
                  variant === "info" && "text-info",
                  variant === "success" && "text-success",
                  variant === "warning" && "text-warning",
                  variant === "destructive" && "text-destructive"
                )}
              >
                {icon ?? <DefaultIcon />}
              </span>
            ) : null}

            <span data-slot="alert-content" className="min-w-0 flex-1">
              {title ? (
                <span data-slot="alert-title" className="block font-medium leading-5">
                  {title}
                </span>
              ) : null}
              {children ? (
                <span
                  data-slot="alert-description"
                  className={cn(
                    "block leading-5 text-muted-foreground",
                    title && "mt-0.5"
                  )}
                >
                  {children}
                </span>
              ) : null}
            </span>

            {action ? (
              <span data-slot="alert-action" className="ml-2 flex shrink-0 items-center">
                {action}
              </span>
            ) : null}

            {closable ? (
              <button
                type="button"
                data-slot="alert-close"
                aria-label="关闭提示"
                className="-mr-1 flex size-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/30 [&_svg]:size-4"
                onClick={() => setVisible(false)}
              >
                <XIcon />
              </button>
            ) : null}
          </div>
        </motion.div>
      ) : null}
    </AnimatePresence>
  )
}

export { Alert, alertVariants }

属性 Props

Alert 支持以下配置属性,并会继承原生 <div> 容器元素的全部 HTML 属性:

属性类型默认值说明
variant"default" | "info" | "success" | "warning" | "destructive""default"提示的语义主题与对应色彩样式,同时决定默认图标及 ARIA 角色。
size"default" | "compact""default"内边距与高度密度预设。compact 适合单行简短提示。
titleReact.ReactNode—提示的主标题,显示在详细描述上方并加粗渲染。
childrenReact.ReactNode—提示的详细描述文本或结构化内容。
iconReact.ReactNode | false—自定义前导图标。传入 `false` 可完全隐藏左侧图标。
actionReact.ReactNode—位于右侧的自定义操作区(如按钮、链接或操作组)。
closablebooleanfalse是否在右上角显示关闭叉号按钮。
visibleboolean—受控模式下的显示与隐藏状态。
defaultVisiblebooleantrue非受控模式下的初始显示状态。
onVisibleChange(visible: boolean) => void—当提示因点击关闭按钮或受控更新而发生可见性改变时触发的回调。
rolestring—覆盖默认的 ARIA 角色属性(默认 warning / destructive 为 alert,其余为 status)。
classNamestring—应用于外层容器的额外 CSS 类名。

事件 Events

属性类型默认值说明
onVisibleChange(visible: boolean) => void—用户点击关闭按钮(`closable`)或受控状态变更时触发,参数为最新的可见布尔值。

使用场景与设计规范

Alert 是页面流内持续可见的静态反馈组件,通常驻留在页面或表单特定位置,向用户传达与当前上下文强相关的状态或警告。

  • Alert vs Message (Toast):
    • Alert:位于页面文档流内,占据实际版面高度,适合承载长段说明、需长期留存的错误排查指南或包含明确纠错操作的卡片。
    • Message:浮动于屏幕顶层(Portal),超时自动消失,适合对保存、复制等即时操作结果进行极简反馈(1~2 秒即可阅读完毕)。
  • 语义色彩规范:
    • info:中性提示、版本发布公告、功能提示。
    • success:流程完成确认、安全合规检查通过。
    • warning:非阻断性预警(如额度快耗尽、证书即将到期)。
    • destructive:阻断性错误、连接失败、数据被删除风险。
  • 操作明确性:尾部 action 按钮应使用明确动词(如“重新连接”、“升级配额”),避免使用含糊的“确定”。

场景示例

尾部操作 (Action)

将“重试”、“更新账单”或“撤销”等下一步关键动作放置在提示右侧:

Loading…

可关闭与受控显示 (Dismissible)

开启 closable 后用户可主动关闭已知悉的公告或临时预警,配合 visible 与 onVisibleChange 实现受控管理。关闭时提示条淡出并收起高度,下方内容平滑补位;重新显示时反向展开。需要与相邻元素保持间距时,把 margin 写在 className 上,间距会随高度一起收起:

Loading…

紧凑单行模式 (Compact)

在表单顶部或空间紧凑的弹窗内,使用 size="compact" 展现无标题的单行轻量提示:

Loading…

自定义图标与隐藏图标

支持传入自定义图标或传入 icon={false} 开启极简纯文字模式:

Loading…

业务场景:安全风险警报与证书预警

在控制台监控与安全中心,组合告警等级、高对比度行动按钮与审计说明文案:

Loading…

无障碍与交互 Accessibility

  • 动态 ARIA 角色分配:
    • warning 与 destructive 变体默认挂载 role="alert" 与 aria-live="assertive",屏幕阅读器会在出现时立即优先播报。
    • info、success 与 default 变体默认挂载 role="status" 与 aria-live="polite",屏幕阅读器会在空闲时平稳朗读。
  • 关闭按钮可访问性:内置关闭按钮配置了 aria-label="关闭提示",支持 Tab 聚焦并通过 Enter / Space 触发关闭。
  • 动效降级:开启系统「减弱动态效果」后,关闭与显示只做淡入淡出,不再过渡高度。
  • 多重感知设计:不单单依靠色彩传达错误,每个语义变体均有特定的图形图标,并在视觉上提供了明确的文字内容。