wui
组件

动态图标 Animated Icon

图标内部路径会随交互运动的 Lucide 动态图标集合。

第三方依赖 · @animateicons/react

基础用法

这些图标不是对整个 SVG 做统一缩放或旋转。每个图标都配有自己的路径动画:铃铛会摆动,复制图层会分离, 菜单线条会重排,齿轮会转动。这样可以让动画和图形含义对应起来。

Loading…

颜色变体

AnimatedIcon 使用与 Icon 相同的语义色变体。颜色通过 currentColor 传给动态图标内部的 SVG 路径,因此不会影响图标本身的动画编排。

Loading…
<AnimatedIcon icon={HeartIcon} variant="success" size={24} />
<AnimatedIcon icon={TriangleAlertIcon} variant="warning" size={24} />
<AnimatedIcon icon={CircleXIcon} variant="destructive" size={24} />

支持 default、primary、secondary、muted、info、success、warning 和 destructive。需要自定义颜色时,传入 color;它会覆盖 variant 的语义色。

<AnimatedIcon icon={HeartIcon} variant="primary" color="#e11d48" />

安装完整集合:

pnpm dlx @wui-design/cli@latest add @wui/animated-icon-collection

完整集合包含 AnimateIcons 的 248 个图标、ItsHover 去重后独有的 207 个图标,以及 SVGlide 的 333 个 Lucide 动态图标。 图标库会按名称过滤重复项,但不同来源仍可能存在风格和参数差异。安装命令会同时加入对应依赖、本地 ItsHover 图标源码 和统一适配组件;只需要少量图标时,按需导入可以减少依赖体积。

components/ui/animated-icon.tsx
"use client"

import * as React from "react"

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

export interface AnimatedIconHandle {
  startAnimation: () => void
  stopAnimation: () => void
}

export interface AnimatedIconGlyphProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  | "color"
  | "onAnimationEnd"
  | "onAnimationIteration"
  | "onAnimationStart"
  | "onDrag"
  | "onDragEnd"
  | "onDragStart"
> {
  /** Icon dimensions in pixels. @default 24 */
  size?: number
  /** Optional animation duration in seconds, when supported by the glyph. */
  duration?: number
  /** Whether the glyph should animate, when supported by the glyph. */
  isAnimated?: boolean
  /** Semantic text color token. @default "default" */
  variant?: AnimatedIconColor
  /** Custom CSS/SVG color. Overrides the semantic color when provided. */
  color?: string
}

const animatedIconColorVariants = {
  default: "text-foreground",
  primary: "text-primary",
  secondary: "text-secondary-foreground",
  muted: "text-muted-foreground",
  info: "text-info",
  success: "text-success",
  warning: "text-warning",
  destructive: "text-destructive",
} as const

export type AnimatedIconColor = keyof typeof animatedIconColorVariants

export type AnimatedIconGlyph = React.ForwardRefExoticComponent<
  AnimatedIconGlyphProps & React.RefAttributes<AnimatedIconHandle>
>

export interface AnimatedIconProps extends AnimatedIconGlyphProps {
  /** Path-animated icon imported from `@animateicons/react/lucide`. */
  icon: AnimatedIconGlyph
  /** Accessible name. Omit it when the icon is purely decorative. */
  label?: string
  /** Imperative playback handle exposed by the animated glyph. */
  ref?: React.Ref<AnimatedIconHandle>
}

/**
 * A common accessible adapter for path-animated icons from AnimateIcons.
 * Every glyph keeps its own semantic motion and imperative playback handle.
 */
function AnimatedIcon({
  icon: Glyph,
  label,
  ref,
  variant = "default",
  className,
  color,
  ...props
}: AnimatedIconProps) {
  return (
    <Glyph
      ref={ref}
      data-slot="animated-icon"
      aria-hidden={label ? undefined : true}
      aria-label={label}
      role={label ? "img" : undefined}
      className={cn(
        color === undefined ? animatedIconColorVariants[variant] : undefined,
        className
      )}
      color={color}
      {...props}
    />
  )
}

export { AnimatedIcon, animatedIconColorVariants }

动态图标库

悬停或键盘聚焦任意图标即可预览动画,点击会复制对应来源的按需导入语句。可以在“全部 / AnimateIcons / ItsHover / SVGlide”之间切换。 顶部的 From / To 预览可直接选择任意两枚图库图标:Play 会以 SVG 路径 morph 从 From 播到 To,随后切换为 Reset。

From
To

704 / 704 个逐路径动态图标

直接使用

import { BellRingIcon, CopyIcon } from "@animateicons/react/lucide"
import {
  AirplaneIcon,
  BrandOpenaiIcon,
} from "@/components/ui/animated-icons"

<BellRingIcon size={24} />
<CopyIcon size={24} duration={0.7} />
<AirplaneIcon size={24} />
<BrandOpenaiIcon size={24} />

每个图标默认在自身悬停时播放。如果交互区域比图标大,例如工具栏按钮或菜单项,建议使用 wui 的 AnimatedIcon, 并通过 ref 让父元素控制动画。这样用户悬停按钮空白区域时,图标也会给出反馈。

SVGlide 使用受控的 data-hovered 属性:

import { useState } from "react"
import { Activity } from "svglide"

export function ActivityButton() {
  const [hovered, setHovered] = useState(false)

  return (
    <button
      onPointerEnter={() => setHovered(true)}
      onPointerLeave={() => setHovered(false)}
      onFocus={() => setHovered(true)}
      onBlur={() => setHovered(false)}
    >
      <Activity data-hovered={hovered} className="size-6" />
      Activity
    </button>
  )
}

从父元素控制

"use client"

import { useRef } from "react"
import { BellRingIcon } from "@animateicons/react/lucide"

import {
  AnimatedIcon,
  type AnimatedIconHandle,
} from "@/components/ui/animated-icon"

export function Notifications() {
  const iconRef = useRef<AnimatedIconHandle>(null)

  return (
    <button
      aria-label="通知"
      onMouseEnter={() => iconRef.current?.startAnimation()}
      onMouseLeave={() => iconRef.current?.stopAnimation()}
      onFocus={() => iconRef.current?.startAnimation()}
      onBlur={() => iconRef.current?.stopAnimation()}
    >
      <AnimatedIcon ref={iconRef} icon={BellRingIcon} size={24} />
    </button>
  )
}

startAnimation() 与 stopAnimation() 适合按钮、菜单项和保存状态等由父元素控制的场景。组件只控制播放时机, 仍会保留每个图标自己的动画编排,不会把所有图标替换成同一种动效。

减少动态效果

AnimateIcons 会读取用户的减少动态效果偏好;本地化的 ItsHover 图标通过统一导出层停止悬停和命令式播放。请从 @/components/ui/animated-icons 导入 ItsHover 图标,不要绕过入口直接引用内部文件。

图标来源

  • AnimateIcons:248 个主集合图标,MIT 许可,通过 npm 按需导入。
  • ItsHover:本地化 207 个去重后的补充图标,保留 Apache 2.0 许可证和来源说明。
  • SVGlide:333 个 Lucide 动态图标,通过 npm 按需导入并使用 data-hovered 控制播放。其仓库 README 与 package.json 的许可证标注不一致,使用时需以项目最终发布的许可证文件为准。

品牌图标的源码许可不代表获得对应品牌的商标授权,实际产品使用前仍需核对品牌规范。

属性

属性类型默认值说明
icon *AnimatedIconGlyph—从 @animateicons/react/lucide 导入的路径动画图标。
labelstring—图标的无障碍名称。纯装饰图标无需设置。
refRef<AnimatedIconHandle>—由动态路径图标暴露的命令式播放控制句柄。
sizenumber24图标尺寸,单位为像素。
durationnumber—图标支持时,单次动画的时长,单位为秒。
isAnimatedboolean—是否播放图标动画,具体效果取决于图标本身是否支持。
variant"default" | "secondary" | "destructive" | "success" | "warning" | "info" | "primary" | "muted"default图标使用的语义文字颜色。
colorstring—自定义 CSS/SVG 颜色。设置后会覆盖 variant。

属性名后的 * 表示必填。