Stepper 步骤条

带有步骤状态与切换校验的步骤导航。

Step 2 of 0
第 2 步,共 3 步
import { Stepper } from '@hina-ui/react'

const items = [
  { title: '步骤 A', description: '第一项的说明' },
  { title: '步骤 B', description: '第二项包含更长的说明文字,支持自然换行' },
  { title: '步骤 C', description: '最后一项的说明' },
]

export default function Demo() {
  return <Stepper items={items} defaultValue={2} />
}
tsx

用法

import { Stepper, type StepperItem } from '@hina-ui/react'
ts

items 定义步骤,value 是从 1 开始的当前步骤编号,切换时调用 onValueChange。默认从第一步开始;不传 value 时由组件管理当前步骤,可用 defaultValue 设置初始值。

Step 1 of 0
第 1 步,共 3 步

当前步骤:1

'use client'

import { useState } from 'react'
import { Stepper, Text, Stack } from '@hina-ui/react'

const items = [
  { title: '步骤 A', description: '第一项的说明' },
  { title: '步骤 B', description: '第二项包含更长的说明文字,支持自然换行' },
  { title: '步骤 C', description: '最后一项的说明' },
]

export default function Demo() {
  const [step, setStep] = useState(1)
  return (
    <Stack className="w-full" gap="lg">
      <Stepper value={step} onValueChange={setStep} items={items} />
      <Text size="sm" tone="muted">
        当前步骤:{step}
      </Text>
    </Stack>
  )
}
tsx

示例

线性与自由切换

linear 默认为 true:可以回到之前的步骤,也可以进入紧邻的下一步,但不能直接跳过中间步骤。设为 false 后,可进入任意未禁用步骤。

线性限制只决定可切换的范围,不代表数据已经通过校验。需要校验时使用 beforeChange。直接从外部修改 value 是调用方主动更新状态,不经过切换限制和校验。

Step 1 of 0
第 1 步,共 3 步
'use client'

import { useState } from 'react'
import { Stepper, Stack, Switch } from '@hina-ui/react'

const items = [
  { title: '步骤 A', description: '第一项的说明' },
  { title: '步骤 B', description: '第二项包含更长的说明文字,支持自然换行' },
  { title: '步骤 C', description: '最后一项的说明' },
]

export default function Demo() {
  const [linear, setLinear] = useState(true)
  return (
    <Stack className="w-full" gap="lg">
      <Switch checked={linear} onCheckedChange={setLinear}>
        线性切换
      </Switch>
      <Stepper items={items} linear={linear} />
    </Stack>
  )
}
tsx

纵向

orientation="vertical" 将文字放在节点旁,连线随说明长度延伸。横向布局将文字放在节点下方,各步骤等宽;方向变化同时影响方向键导航。

Step 2 of 0
第 2 步,共 3 步
import { Stepper } from '@hina-ui/react'

const items = [
  { title: '步骤 A', description: '第一项的说明' },
  { title: '步骤 B', description: '第二项包含更长的说明文字,支持自然换行' },
  { title: '步骤 C', description: '最后一项的说明' },
]

export default function Demo() {
  return <Stepper items={items} defaultValue={2} orientation="vertical" className="max-w-md" />
}
tsx

状态

当前步骤之前的步骤默认显示完成标记;completed 可显式标记完成,包括最后一步。error 优先显示错误标记,当前步骤仍保留其当前位置语义。

单项 disabled 禁止进入该步骤,根 disabled 禁止所有用户切换。next()、prev() 不会跳过禁用项;相邻项被禁用时,对应方法返回 false。

Step 2 of 0
第 2 步,共 3 步
'use client'

import { useState } from 'react'
import { Stepper, Stack, Switch } from '@hina-ui/react'

const items = [
  { title: '已完成', completed: true },
  { title: '需要检查', description: '此步骤存在错误', error: true },
  { title: '已禁用', disabled: true },
]

export default function Demo() {
  const [disabled, setDisabled] = useState(false)
  return (
    <Stack className="w-full" gap="lg">
      <Switch checked={disabled} onCheckedChange={setDisabled}>
        禁用整个步骤条
      </Switch>
      <Stepper items={items} defaultValue={2} linear={false} disabled={disabled} />
    </Stack>
  )
}
tsx

内容与导航

children 可以是函数,接收当前步骤与导航方法。可组合 Card、Button 等组件;只有提供 children 时才渲染内容区。

内容区始终挂载,如何切换、保留或重置内部内容由调用方决定。组件 ref 也提供 next()、prev()、goTo(step),与 children 收到的方法行为一致。

Step 1 of 0

步骤 A

这里是第 1 步的内容。

第 1 步,共 3 步
'use client'

import { Stepper, Button, Card, Inline, Stack, Text } from '@hina-ui/react'

const items = [
  { title: '步骤 A', description: '第一项的说明' },
  { title: '步骤 B', description: '第二项包含更长的说明文字,支持自然换行' },
  { title: '步骤 C', description: '最后一项的说明' },
]

export default function Demo() {
  return (
    <Stepper items={items}>
      {({ item, step, next, prev, canNext, canPrev }) => (
        <Card>
          <Stack gap="lg">
            <Text weight="medium">{item?.title}</Text>
            <Text tone="muted">这里是第 {step} 步的内容。</Text>
            <Inline justify="between">
              <Button variant="outline" tone="neutral" disabled={!canPrev} onClick={prev}>
                上一步
              </Button>
              <Button disabled={!canNext} onClick={next}>
                下一步
              </Button>
            </Inline>
          </Stack>
        </Card>
      )}
    </Stepper>
  )
}
tsx

切换前校验

beforeChange(nextStep, previousStep) 在点击步骤、键盘激活或调用导航方法时执行。返回 false 阻止切换;返回 true 或不返回值允许切换,也可返回 Promise。

等待期间保持当前步骤并阻止重复切换。抛错或 Promise 拒绝时保留当前步骤,通过 onError 回调交给调用方处理。外部修改当前步骤、步骤结构或禁用状态,以及组件卸载后,旧校验结果不会再推进步骤。

示例使用 FormField 和 Input。点击第二步的标题与点击“下一步”会经过同一个校验函数。

Step 1 of 0
第 1 步,共 3 步
'use client'

import { useState } from 'react'
import { Stepper, Button, Card, FormField, Input, Inline, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [name, setName] = useState('')
  const [error, setError] = useState('')
  const items = [{ title: '步骤 A', error: !!error }, { title: '步骤 B' }, { title: '步骤 C' }]

  async function beforeChange(step: number, previousStep: number) {
    if (step <= previousStep || previousStep !== 1) return true
    await new Promise(resolve => setTimeout(resolve, 700))
    const message = name.trim() ? '' : '请填写名称'
    setError(message)
    return !message
  }

  return (
    <Stepper items={items} beforeChange={beforeChange}>
      {({ step, next, prev, pending, canNext, canPrev }) => (
        <Card>
          <Stack gap="lg">
            {step === 1 ? (
              <FormField label="名称" error={error}>
                <Input value={name} onValueChange={setName} disabled={pending} />
              </FormField>
            ) : (
              <Text>第 {step} 步</Text>
            )}
            <Inline justify="between">
              <Button variant="outline" tone="neutral" disabled={!canPrev} onClick={prev}>
                上一步
              </Button>
              <Button loading={pending} disabled={!canNext} onClick={next}>
                下一步
              </Button>
            </Inline>
          </Stack>
        </Card>
      )}
    </Stepper>
  )
}
tsx

自定义指示器与文字

renderIndicator、renderTitle、renderDescription 接收原始 item、从 0 开始的 index、从 1 开始的 step、state、active、pending 与 disabled。自定义字段会保留类型推导。

示例用图标替换默认编号,并在标题中加入 Tag。这些函数返回的内容位于步骤按钮内部,应只放非交互内容;输入框、链接和其他按钮放在 children 中。

Step 1 of 0
第 1 步,共 3 步
'use client'

import { FileText, ListChecks, Send } from 'lucide-react'
import { Stepper, Tag, Inline, Text, type StepperItem } from '@hina-ui/react'

interface Item extends StepperItem {
  note: string
}
const icons = [FileText, ListChecks, Send]
const items: Item[] = [
  { title: '步骤 A', note: '必填' },
  { title: '步骤 B', note: '可选' },
  { title: '步骤 C', note: '确认' },
]

export default function Demo() {
  return (
    <Stepper
      items={items}
      linear={false}
      renderIndicator={({ index }) => {
        const Icon = icons[index]!
        return <Icon />
      }}
      renderTitle={({ item, active }) => (
        <Inline as="span" justify="center" gap="sm">
          <Text as="span" weight="medium">
            {item.title}
          </Text>
          <Tag tone={active ? 'accent' : 'neutral'}>{item.note}</Tag>
        </Inline>
      )}
    />
  )
}
tsx

尺寸

size 调整指示器与文字大小。节点尺寸和间距使用 Hina 变量,随密度设置变化。

sm

Step 2 of 0
第 2 步,共 3 步

md

Step 2 of 0
第 2 步,共 3 步

lg

Step 2 of 0
第 2 步,共 3 步
import { Stepper, Stack, Text, type StepperSize } from '@hina-ui/react'

const sizes: StepperSize[] = ['sm', 'md', 'lg']
const items = [
  { title: '步骤 A', description: '第一项的说明' },
  { title: '步骤 B', description: '第二项包含更长的说明文字,支持自然换行' },
  { title: '步骤 C', description: '最后一项的说明' },
]

export default function Demo() {
  return (
    <Stack className="w-full" gap="lg">
      {sizes.map(size => (
        <Stack key={size}>
          <Text size="sm" tone="muted">
            {size}
          </Text>
          <Stepper items={items} size={size} defaultValue={2} />
        </Stack>
      ))}
    </Stack>
  )
}
tsx

RTL

方向继承外层 dir 或 ConfigProvider,也可用 dir="rtl" 指定。横向排列、连线和方向键导航共同反转。

Step 1 of 0
第 1 步,共 3 步
import { Stepper } from '@hina-ui/react'

const items = [
  { title: 'الخطوة الأولى', description: 'وصف الخطوة الأولى' },
  { title: 'الخطوة الثانية', description: 'وصف الخطوة الثانية' },
  { title: 'الخطوة الثالثة' },
]

export default function Demo() {
  return <Stepper items={items} linear={false} dir="rtl" />
}
tsx

行为与无障碍

  • 步骤组成有名称的列表组,label 可替换默认无障碍名称。
  • 每一步是真实按钮,标题和可选描述参与命名;当前按钮带 aria-current="step"。错误和完成状态同时提供文字,不仅依靠颜色与图标。
  • Tab 在可操作步骤和内容之间移动;横向用左右方向键,纵向用上下方向键移动焦点,Enter 或空格激活。移动焦点不会自动切换步骤。
  • 导航不会主动抢走内容区焦点,也不会自动提交表单。
  • 进度播报随界面语言变化。减弱动态效果设置由 Hina 动效变量统一处理。

API

Props

属性
类型
默认值
说明
items
T[]
必填
步骤列表,T extends StepperItem
value / onValueChange
number
—
当前步骤,编号从 1 开始
defaultValue
number
1
非受控初始步骤
orientation
'horizontal' | 'vertical'
'horizontal'
排布与键盘导航方向
size
'sm' | 'md' | 'lg'
'md'
尺寸
linear
boolean
true
限制顺序切换
disabled
boolean
false
禁止所有用户切换
beforeChange
StepperBeforeChange
—
切换前校验
label
string
取自界面语言
步骤组的无障碍名
dir
'ltr' | 'rtl'
继承
阅读方向
className
string
—
根元素的类

空列表的显示步骤为 0;越界或无效的当前值仅在呈现时收敛到有效范围,不会自动调用 onValueChange 写回。defaultValue 只用于初始化。

StepperItem

字段
类型
说明
title
string
必填,步骤标题
description
string
可选说明
disabled
boolean
禁止进入该步骤
completed
boolean
显式显示完成;设为 false 不覆盖之前步骤的自动完成态
error
boolean
错误状态,优先于完成态

步骤编号和身份由数组位置决定;index 从 0 开始,step 从 1 开始。

内容属性

属性
参数
说明
children
StepperNavigation<T>
当前内容与导航操作
renderIndicator
StepperSlotProps<T>
节点,默认编号、勾、错误图标或等待指示
renderTitle
StepperSlotProps<T>
标题
renderDescription
StepperSlotProps<T>
说明

StepperSlotProps<T> 为 { item, index, step, state, active, pending, disabled }。state 是 'inactive' | 'active' | 'completed' | 'error';pending 表示该项正在等待切换校验,disabled 表示该项被显式禁用或被线性范围限制。

StepperNavigation<T> 为 { step, item, total, pending, canNext, canPrev, next, prev, goTo }。空列表时 item 为 undefined。导航方法返回 Promise<boolean>,实际发起步骤更新时为 true,被阻止、无变化或校验过期时为 false。

回调

回调
参数
说明
onValueChange
step: number
请求更新当前步骤
onError
error: unknown
切换校验抛出的异常

Ref

step、pending、canNext、canPrev 和 next()、prev()、goTo(step) 可通过 ref 访问。beforeChange 为 (nextStep: number, previousStep: number) => boolean | void | Promise<boolean | void>。