Multi-step Form
多步驟表單容器,方向感知的滑動過場搭配可切換款式的進度指示。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add "https://webberui.com/r/multi-step-form.json?t=<安裝 token>"Multi-step Form 是用 React、Motion(motion/react)與 Tailwind CSS 做的多步驟表單(multi-step form/表單精靈 wizard)容器:用 MultiStepFormStep 宣告步驟,切換時依前進或後退方向滑動,進度指示可選進度條、圓點或編號步驟三種款式。元件只負責步驟流程與過場,欄位與驗證由你自己的表單邏輯決定。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<MultiStepForm/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- SaaS 註冊流程:帳號、公司資料、方案偏好分成幾步填寫
- 結帳流程:收件資訊、配送方式、付款確認
- 新使用者的 onboarding 問卷,每頁只問一兩個問題
- 報名表、申請表這類欄位很多、需要分段降低填寫壓力的表單
- 不適合:只需要顯示流程進度、內容不在同一個容器裡切換時,改用 Stepper
安裝
npx webberui@latest add "https://webberui.com/r/multi-step-form.json?t=<安裝 token>"或在 components.json 設定 registries 後,改用 @webberui/multi-step-form 安裝。
使用
以 <MultiStepFormStep> 宣告每一個步驟,容器會依目前步驟做方向感知的滑動過場,並自動渲染進度指示與上一步/下一步控制列。
import {
MultiStepForm,
MultiStepFormStep,
} from "@/components/ui/multi-step-form";
<MultiStepForm variant="steps" onComplete={() => console.log("done")}>
<MultiStepFormStep label="帳號">{/* ...欄位 */}</MultiStepFormStep>
<MultiStepFormStep label="個人資料">{/* ...欄位 */}</MultiStepFormStep>
<MultiStepFormStep label="確認">{/* ...摘要 */}</MultiStepFormStep>
</MultiStepForm>;自訂控制列
將 controls 設為 false,改用 useMultiStepForm() hook 在任意位置自組導覽(可加入驗證後才 next()):
import { useMultiStepForm } from "@/components/ui/multi-step-form";
function Footer() {
const { back, next, isFirst, isLast } = useMultiStepForm();
return (
<div>
<button onClick={back} disabled={isFirst}>
上一步
</button>
<button onClick={next}>{isLast ? "送出" : "下一步"}</button>
</div>
);
}受控模式
傳入 step 與 onStepChange 即可完全掌控目前步驟,適合搭配表單驗證做步驟守門。
const [step, setStep] = React.useState(0);
<MultiStepForm step={step} onStepChange={(next) => setStep(next)}>
{/* ...steps */}
</MultiStepForm>;範例
驗證通過才能前進
受控模式下,onStepChange 會帶入方向;往前走(direction === 1)時先檢查目前步驟,不通過就不更新 step。最後一步按「完成」走的是 onComplete,不會經過 onStepChange。
"use client";
import * as React from "react";
import {
MultiStepForm,
MultiStepFormStep,
} from "@/components/ui/multi-step-form";
export function SignupForm() {
const [step, setStep] = React.useState(0);
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState("");
return (
<MultiStepForm
step={step}
onStepChange={(next, direction) => {
if (direction === 1 && step === 0 && !email.includes("@")) {
setError("請輸入有效的電子郵件");
return;
}
setError("");
setStep(next);
}}
onComplete={() => console.log("送出", { email })}
>
<MultiStepFormStep label="帳號">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@company.com"
className="w-full rounded-lg border px-3 py-2 text-sm"
/>
{error ? <p className="mt-2 text-sm text-red-600">{error}</p> : null}
</MultiStepFormStep>
<MultiStepFormStep label="公司資料">{/* 公司名稱、人數 */}</MultiStepFormStep>
<MultiStepFormStep label="確認">{/* 摘要 */}</MultiStepFormStep>
</MultiStepForm>
);
}進度條款式與自訂按鈕文字
variant="bar" 顯示目前步驟標題與「x / y」計數;三個 label props 改內建控制列的文字。
<MultiStepForm
variant="bar"
backLabel="返回"
nextLabel="繼續"
completeLabel="送出訂單"
onComplete={placeOrder}
>
<MultiStepFormStep label="收件資訊">{/* ... */}</MultiStepFormStep>
<MultiStepFormStep label="配送方式">{/* ... */}</MultiStepFormStep>
<MultiStepFormStep label="付款確認">{/* ... */}</MultiStepFormStep>
</MultiStepForm>從指定步驟開始
非受控模式下用 defaultStep 指定初始步驟,例如使用者回來繼續填寫草稿時。索引超出範圍會自動夾回有效值。
<MultiStepForm defaultStep={savedDraft.step} variant="dots">
{/* ...steps */}
</MultiStepForm>Props
MultiStepForm
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | ReactNode | — | 步驟宣告,皆為 <MultiStepFormStep> |
step | number | — | 受控目前步驟索引;不傳則為非受控 |
defaultStep | number | 0 | 非受控模式的初始步驟索引 |
onStepChange | (step: number, direction: 1 | -1) => void | — | 步驟變更時觸發,帶入新索引與方向 |
onComplete | () => void | — | 於最後一步按下「完成」時觸發 |
variant | "bar" | "dots" | "steps" | false | "steps" | 進度指示款式;false 不顯示 |
controls | boolean | true | 是否渲染內建控制列 |
backLabel | string | "上一步" | 上一步按鈕文字 |
nextLabel | string | "下一步" | 下一步按鈕文字 |
completeLabel | string | "完成" | 末步按鈕文字 |
MultiStepFormStep
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
label | string | — | 進度指示顯示的步驟標題 |
children | ReactNode | — | 步驟內容 |
className | string | — | 追加到步驟容器的 className |
useMultiStepForm()
回傳 { step, total, direction, isFirst, isLast, goTo, next, back, reducedMotion },供自訂控制列或進度指示使用。
細節
- 三種進度款式:
bar進度條、dots圓點(目前點延伸為膠囊)、steps編號步驟(完成打勾、連接線依進度上色)。 - 方向感知:前進時新步驟自右側滑入、舊步驟往左退場;後退則相反。方向於 render 期間依前次索引推導,受控與非受控皆正確。
- 檢視區採
overflow-hidden裁切水平滑動,並以popLayout讓退場步驟脫離流排、由進場步驟決定容器高度,避免過場時高度塌陷。
可及性
- 進度指示中「目前與先前」的步驟可點擊回跳(
goTo),未到達的步驟停用,維持填寫順序。 - 目前步驟標上
aria-current="step";進度條提供role="progressbar"與aria-valuenow。 - 步驟切換透過
aria-live="polite"區塊朗讀「步驟 x / y」。 - 內建控制列為原生
button,可鍵盤操作並具focus-visible外框;第一步時上一步鈕停用。 - 使用者系統開啟「減少動態效果」時,滑動退化為短暫淡入淡出,進度填充直接跳位。
常見問題
可以在 Next.js App Router 使用嗎?
可以。檔案開頭已有 "use client",在 Server Component 頁面裡照常 import;需要用 state 或事件處理的包裝元件,自己也要標 "use client"。
可以搭配 React Hook Form 之類的表單函式庫嗎?
可以。元件不管理欄位值,把整個 MultiStepForm 包在你的 <form> 或表單 provider 裡,再用受控模式或 useMultiStepForm() 的 next() 在驗證後切換步驟。內建控制列的按鈕是 type="button",不會意外送出表單。
為什麼我放在 MultiStepForm 裡的其他元素沒有顯示?
容器只會擷取直接子節點中的 MultiStepFormStep,其他節點會被忽略。標題、說明文字請放在步驟內容裡,或放在 MultiStepForm 外面。
螢幕閱讀器會知道步驟換了嗎?
會。容器內有 aria-live="polite" 區塊朗讀「步驟 x / y」與步驟標題,目前步驟標上 aria-current="step"。系統開啟「減少動態效果」時,滑動改為短暫淡入淡出。
需要哪些依賴?
motion、lucide-react、clsx 與 tailwind-merge,詳見上方安裝區塊。