wui
组件

文本高亮 Text Highlight

进入视口、悬停或挂载时,在文字背后划出马克笔式的高亮,跨行文本会逐行依次划过。

第三方依赖 · motion

基础用法

用 TextHighlight 包裹段落中的关键短语,滚动进入视口时高亮会从左向右划过。多个高亮可以通过 delay 依次出现,引导阅读顺序:

Loading…

安装与引入

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

pnpm dlx @wui-design/cli@latest add @wui/text-highlight
安装基础依赖与 Motion 动效库
pnpm add motion clsx tailwind-merge
复制组件源码到 components/ui/text-highlight.tsx
components/ui/text-highlight.tsx
"use client"

import * as React from "react"
import {
  motion,
  useInView,
  useReducedMotion,
  type HTMLMotionProps,
  type Transition,
  type UseInViewOptions,
} from "motion/react"

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

export interface TextHighlightProps
  extends Omit<HTMLMotionProps<"span">, "children" | "transition" | "color"> {
  /** Text to highlight. Wrapped lines are swept one after another. */
  children: React.ReactNode
  /** When the sweep plays. @default "inView" */
  trigger?: "inView" | "hover" | "always"
  /** Controlled highlight state, e.g. driven by hovering a parent row. Overrides `trigger`. */
  active?: boolean
  /** `block` covers the full line height, `underline` a marker band at the bottom. @default "block" */
  variant?: "block" | "underline"
  /** Highlight colour. Any CSS colour or variable. */
  color?: string
  /** Side the sweep starts from. @default "left" */
  from?: "left" | "right"
  /** Seconds before the sweep starts. @default 0 */
  delay?: number
  /** Seconds the sweep takes. @default 0.8 */
  duration?: number
  /** Transition of the sweep; overrides `delay` and `duration`. */
  transition?: Transition
  /** Play only the first time the text enters the viewport. @default true */
  once?: boolean
  /** Intersection options used when `trigger` is `inView`. */
  viewOptions?: Omit<UseInViewOptions, "once">
}

const bandHeight = { block: "100%", underline: "38%" } as const

/**
 * Sweeps a marker-style highlight behind inline text when it scrolls into
 * view, on hover, or immediately.
 */
function TextHighlight({
  children,
  trigger = "inView",
  active: activeProp,
  variant = "block",
  color = "color-mix(in oklab, var(--primary) 12%, transparent)",
  from = "left",
  delay = 0,
  duration = 0.8,
  transition,
  once = true,
  viewOptions,
  className,
  style,
  onPointerEnter,
  onPointerLeave,
  ...props
}: TextHighlightProps) {
  const ref = React.useRef<HTMLSpanElement>(null)
  const reduceMotion = useReducedMotion()
  const inView = useInView(ref, { ...viewOptions, once })
  const [hovered, setHovered] = React.useState(false)

  const active =
    activeProp ??
    (trigger === "always" ? true : trigger === "hover" ? hovered : inView)
  const height = bandHeight[variant]

  return (
    <motion.span
      ref={ref}
      data-slot="text-highlight"
      data-active={active ? "" : undefined}
      className={cn("bg-no-repeat", className)}
      style={{
        backgroundImage: `linear-gradient(${color}, ${color})`,
        backgroundPosition: `${from === "left" ? "0%" : "100%"} 100%`,
        ...style,
      }}
      initial={{ backgroundSize: `0% ${height}` }}
      animate={{ backgroundSize: `${active ? 100 : 0}% ${height}` }}
      transition={
        reduceMotion
          ? { duration: 0 }
          : {
              duration,
              delay: active ? delay : 0,
              ease: [0.22, 1, 0.36, 1],
              ...transition,
            }
      }
      onPointerEnter={(event) => {
        setHovered(true)
        onPointerEnter?.(event)
      }}
      onPointerLeave={(event) => {
        setHovered(false)
        onPointerLeave?.(event)
      }}
      {...props}
    >
      {children}
    </motion.span>
  )
}

export { TextHighlight }

属性 Props

TextHighlight 渲染为行内 <span>,支持以下配置属性:

属性类型默认值说明
childrenReact.ReactNode—需要高亮的行内内容。跨行时高亮会先划过第一行,再依次划过后续行。
trigger"inView" | "hover" | "always""inView"触发时机:进入视口、悬停在文字上,或挂载后立即播放。
activeboolean—受控的高亮状态,传入后覆盖 `trigger`。常用于由整行悬停或焦点驱动高亮。
variant"block" | "underline""block"block 覆盖整行高度;underline 只在文字下部画一条马克笔色带。
colorstring"color-mix(in oklab, var(--primary) 12%, transparent)"高亮颜色,支持任意 CSS 颜色或变量。建议使用半透明色,保证文字对比度。
from"left" | "right""left"高亮划出的起始方向。
delaynumber0高亮开始前的延迟(秒)。
durationnumber0.8划过全部文字所用的时长(秒)。
oncebooleantrue`inView` 模式下是否只播放一次。关闭后离开视口时高亮会收回。
viewOptionsUseInViewOptions—视口检测参数,例如 `{ amount: 1 }` 表示完全可见时才触发。
transitionTransition—自定义过渡参数,会覆盖 `delay` 与 `duration`。

使用场景与设计规范

TextHighlight 适合长文、案例研究与产品介绍中的结论性语句,也可以作为链接、目录项的悬停反馈。

  • 少而精:一个段落中的高亮不宜超过两到三处,否则会失去强调作用。
  • 颜色保持低饱和:默认使用主题色的 12% 透明度。若使用品牌色或黄色马克笔效果,请确认暗色模式下文字仍清晰可读。
  • 不替代语义强调:高亮只是视觉效果,重要内容仍应使用 <strong> 或 <mark> 等语义元素包裹。

场景示例

悬停整行触发

通过 active 由整行链接的悬停与焦点状态驱动高亮,配合 underline 变体作为指南列表的悬停反馈:

Loading…

无障碍与交互 Accessibility

  • 不影响文字本身:高亮通过背景图绘制,不改变文字节点,读屏软件与复制粘贴的内容保持不变。
  • 键盘焦点:由悬停驱动的高亮建议同时响应 focus / blur,让键盘用户获得同样的反馈,如“悬停整行触发”示例所示。
  • 减少动态效果:开启 prefers-reduced-motion 时,高亮直接以最终状态显示,不播放划过动画。