Highlight 高亮块

在活动条目之间平移的高亮块。

'use client'

import { useId, useState } from 'react'
import { Button, Highlight, Inline } from '@hina-ui/react'

const items = ['全部', 'Galgame', '轻小说', '漫画']

export default function Demo() {
  const [current, setCurrent] = useState(0)
  const id = useId()

  return (
    <Inline gap="none" className="bg-inset relative isolate rounded-md p-1">
      {items.map((item, index) => (
        <Button
          key={item}
          variant="ghost"
          tone="neutral"
          size="sm"
          aria-current={current === index ? 'true' : undefined}
          className={current === index ? 'z-0' : 'z-[1]'}
          onClick={() => setCurrent(index)}
        >
          {current === index && (
            <Highlight
              id={id}
              axis="x"
              className="bg-surface absolute inset-0 -z-10 rounded-md shadow-sm"
            />
          )}
          {item}
        </Button>
      ))}
    </Inline>
  )
}
tsx

用法

import { Highlight } from '@hina-ui/react'
ts

Highlight 用于标示当前条目。它不进行任何测量:调用方通过 className 把它定位在活动条目的范围内,活动条目变化时,它由 Motion 的布局动画从原位置连续平移到新位置,而不是在一处消失、在另一处出现。

组件本身没有外观,背景色、圆角与层级全部由调用方通过 className 指定。

在条目之间移动

将它渲染在当前活动条目的内部,按活动状态条件渲染(例如 {active && <Highlight id={id} />}),并提供一个 id。活动条目变化时,原有实例卸载,新实例在新条目内挂载,相同的 id 使 Motion 把它从原位置平移到新位置。id 应由 React 的 useId() 生成,以保证同一页面上的多个实例互不干扰。

页首的示例即为这种用法:每个按钮内部都可以渲染一个 absolute inset-0 的高亮块,只有活动的按钮实际渲染它。

位移动画轴向

设置 axis="x" 只对水平位移做过渡,axis="y" 只对垂直位移做过渡。另一轴的位置直接跟随当前布局,宽高变化仍保留动画。默认 axis="both" 对两个方向的位移都做过渡。

Tabs 与 SegmentedControl 根据 orientation 自动选择轴向;Anchor 使用 y。

覆盖一段范围

将列表排成单列网格,每一项显式占据一行,高亮块同样作为网格项,通过 grid-row 覆盖整段。此时它保持挂载,跨行范围变化时由布局动画连续伸缩。

简介角色制作人员发售信息
'use client'

import { useState } from 'react'
import { Button, Grid, Highlight, Inline, Stack, Text } from '@hina-ui/react'

const rows = ['简介', '角色', '制作人员', '发售信息']

export default function Demo() {
  const [from, setFrom] = useState(1)
  const [to, setTo] = useState(2)
  const span = `${Math.min(from, to) + 1} / ${Math.max(from, to) + 2}`

  return (
    <Stack gap="md" className="w-full max-w-sm">
      <Inline gap="sm">
        <Button
          size="sm"
          variant="outline"
          tone="neutral"
          onClick={() => setFrom((from + 1) % rows.length)}
        >
          移动起点
        </Button>
        <Button
          size="sm"
          variant="outline"
          tone="neutral"
          onClick={() => setTo((to + 1) % rows.length)}
        >
          移动终点
        </Button>
      </Inline>

      <Grid cols={1} gap="none" className="border-line relative isolate rounded-lg border p-1">
        <Highlight
          axis="y"
          style={{ gridRow: span }}
          className="bg-accent-soft col-start-1 -z-10 rounded-md"
        />
        {rows.map((row, index) => (
          <Text
            key={row}
            as="span"
            size="sm"
            className="col-start-1 block px-3 py-1.5"
            style={{ gridRow: index + 1 }}
          >
            {row}
          </Text>
        ))}
      </Grid>
    </Stack>
  )
}
tsx

行为

  • 静止时的位置由 CSS 布局决定,随条目尺寸变化,不需要测量,也不需要监听尺寸变化。
  • 移动与伸缩都是连续的位移,缩放过程中圆角与阴影的变形由 Motion 自动修正。
  • 系统启用减弱动态效果时,直接呈现在目标位置,不做位移过渡。
  • 位移过程中高亮块位于目标条目内部。若各条目各自构成层叠上下文,应给非活动条目更高的层级(例如 z-[1]),活动条目使用 z-0,使高亮块从其他条目的文字下方经过。
  • 服务端渲染输出的就是真实的高亮块,首屏与水合后的状态一致。

无障碍

  • 组件带有 aria-hidden,仅作装饰,不进入无障碍树。
  • 当前位置必须另有语义表达,例如活动条目上的 aria-current 或者 aria-pressed,不能仅依赖高亮块。

API

Props

属性
类型
默认值
说明
id
string
—
共享布局动画的标识,在条目之间移动时必须提供
axis
'x' | 'y' | 'both'
'both'
位移动画的轴向,不影响宽高动画
as
'div' | 'li'
'div'
渲染的元素
className
string
—
追加至根元素的类名