マルチステップ・ウィザード
アカウント→プロフィール→確認の3ステップに分けた入力フォーム。上部の進捗インジケータと次へ/戻るボタンでスライド遷移し、最後に入力内容をまとめて確認できます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<div class="wiz-card">
<div class="fd-brand"><span class="fd-mark">◇</span> FlowDesk セットアップ</div>
<!-- 上部のステップ進捗インジケータ -->
<ol class="wiz-steps" id="wiz-steps">
<li class="wiz-step is-active" data-step="0">
<span class="wiz-dot">1</span>
<span class="wiz-label">アカウント</span>
</li>
<li class="wiz-step" data-step="1">
<span class="wiz-dot">2</span>
<span class="wiz-label">ワークスペース</span>
</li>
<li class="wiz-step" data-step="2">
<span class="wiz-dot">3</span>
<span class="wiz-label">確認</span>
</li>
</ol>
<!-- スライドするパネル群 -->
<div class="wiz-viewport">
<form class="wiz-track" id="wiz-track" novalidate>
<!-- ステップ1: アカウント -->
<section class="wiz-panel" aria-label="アカウント">
<label class="wiz-field">
<span>仕事用メールアドレス</span>
<input type="email" name="email" placeholder="you@company.com" autocomplete="off">
</label>
<label class="wiz-field">
<span>パスワード</span>
<input type="password" name="password" placeholder="8文字以上" autocomplete="off">
</label>
</section>
<!-- ステップ2: ワークスペース -->
<section class="wiz-panel" aria-label="ワークスペース">
<label class="wiz-field">
<span>ワークスペース名</span>
<input type="text" name="name" placeholder="例: 営業チーム" autocomplete="off">
</label>
<label class="wiz-field">
<span>契約プラン</span>
<select name="plan">
<option value="スターター">スターター</option>
<option value="ビジネス">ビジネス</option>
<option value="エンタープライズ">エンタープライズ</option>
</select>
</label>
</section>
<!-- ステップ3: 確認サマリ -->
<section class="wiz-panel" aria-label="確認">
<p class="wiz-summary-head">入力内容の確認</p>
<dl class="wiz-summary" id="wiz-summary"></dl>
</section>
</form>
</div>
<!-- 操作ボタン -->
<div class="wiz-nav">
<button type="button" class="wiz-btn ghost" id="wiz-prev" disabled>戻る</button>
<button type="button" class="wiz-btn solid" id="wiz-next">次へ</button>
</div>
<p class="wiz-status" id="wiz-status" role="status"></p>
</div>
CSS
:root {
/* FlowDeskテーマ用CSS変数 */
--accent: #4f7cff;
--accent2: #2f5fe0;
--line: rgba(120, 150, 220, 0.28);
--ink: #e8edf7;
--muted: #8ea0c4;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background:
radial-gradient(80% 60% at 50% 0%, rgba(79, 124, 255, 0.32), transparent 60%),
#0f1b34;
color: var(--ink);
}
.wiz-card {
width: min(420px, 94vw);
padding: 20px 22px 16px;
background: #16244a;
border: 1px solid rgba(120, 150, 220, 0.22);
border-radius: 16px;
box-shadow: 0 26px 60px -24px rgba(0, 0, 0, 0.8);
}
.fd-brand {
display: flex; align-items: center; gap: 7px; margin-bottom: 16px;
font-size: 0.8rem; font-weight: 700; letter-spacing: 0.05em; color: #9fb6f0;
}
.fd-mark { color: #4f7cff; font-size: 1rem; }
/* 進捗インジケータ */
.wiz-steps {
list-style: none; display: flex; justify-content: space-between;
margin: 0 0 20px; padding: 0; position: relative;
}
.wiz-steps::before {
content: ""; position: absolute; top: 14px; left: 14px; right: 14px;
height: 2px; background: var(--line);
}
.wiz-step { position: relative; z-index: 1; display: grid; justify-items: center; gap: 6px; flex: 1; }
.wiz-dot {
width: 28px; height: 28px;
display: grid; place-items: center;
font-size: 0.82rem; font-weight: 700; color: var(--muted);
background: #16244a; border: 2px solid var(--line); border-radius: 50%;
transition: all 0.25s ease;
}
.wiz-label { font-size: 0.72rem; color: var(--muted); font-weight: 600; transition: color 0.25s ease; }
.wiz-step.is-active .wiz-dot {
color: #fff; border-color: transparent;
background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 8px 18px -8px rgba(79, 124, 255, 0.85);
}
.wiz-step.is-active .wiz-label { color: var(--ink); }
.wiz-step.is-done .wiz-dot { color: #fff; border-color: transparent; background: #34d399; font-size: 0; }
.wiz-step.is-done .wiz-dot::after { content: "✓"; font-size: 0.9rem; }
/* スライド領域 */
.wiz-viewport { overflow: hidden; }
.wiz-track {
display: flex; width: 300%; transform: translateX(0);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.wiz-panel {
width: 33.3333%; flex: 0 0 33.3333%;
padding: 4px 2px; display: grid; gap: 12px;
align-content: start; min-height: 152px;
}
.wiz-field { display: grid; gap: 5px; }
.wiz-field span { font-size: 0.76rem; font-weight: 600; color: var(--muted); }
.wiz-field input,
.wiz-field select {
width: 100%; padding: 10px 12px;
font-size: 0.9rem; color: var(--ink);
background: #0f1b34;
border: 2px solid var(--line); border-radius: 10px; outline: none;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.wiz-field input::placeholder { color: #5f719a; }
.wiz-field input:focus,
.wiz-field select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(79, 124, 255, 0.2);
}
.wiz-field input.invalid { border-color: #f87171; }
/* 確認サマリ */
.wiz-summary-head { margin: 2px 0 4px; font-size: 0.9rem; font-weight: 700; color: var(--ink); }
.wiz-summary { margin: 0; display: grid; gap: 8px; }
.wiz-row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 12px; background: #0f1b34; border-radius: 10px; }
.wiz-row dt { margin: 0; font-size: 0.76rem; color: var(--muted); font-weight: 600; }
.wiz-row dd { margin: 0; font-size: 0.84rem; color: var(--ink); font-weight: 600; text-align: right; word-break: break-all; }
/* ナビゲーション */
.wiz-nav { display: flex; gap: 10px; margin-top: 16px; }
.wiz-btn {
flex: 1; padding: 11px 14px;
font-size: 0.88rem; font-weight: 700;
border: none; border-radius: 11px; cursor: pointer;
transition: transform 0.1s ease, opacity 0.16s ease;
}
.wiz-btn.solid {
color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 12px 22px -12px rgba(79, 124, 255, 0.85);
}
.wiz-btn.ghost { color: var(--muted); background: rgba(120, 150, 220, 0.14); }
.wiz-btn:not(:disabled):hover { transform: translateY(-1px); }
.wiz-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.wiz-status { margin: 10px 0 0; min-height: 16px; text-align: center; font-size: 0.78rem; font-weight: 600; color: var(--accent); }
.wiz-status.ok { color: #34d399; }
.wiz-status.err { color: #f87171; }
@media (prefers-reduced-motion: reduce) {
.wiz-track, .wiz-dot, .wiz-btn, .wiz-field input, .wiz-field select { transition: none; }
}
JavaScript
// 要素取得(null安全)
const track = document.getElementById("wiz-track");
const stepsEl = document.getElementById("wiz-steps");
const prevBtn = document.getElementById("wiz-prev");
const nextBtn = document.getElementById("wiz-next");
const statusEl = document.getElementById("wiz-status");
const summaryEl = document.getElementById("wiz-summary");
if (track && stepsEl && prevBtn && nextBtn) {
const steps = [...stepsEl.querySelectorAll(".wiz-step")];
const TOTAL = steps.length; // 全3ステップ
let current = 0;
// 各ステップの必須チェック定義
const validators = [
() => {
const email = track.elements.email;
const pw = track.elements.password;
const okEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
const okPw = pw.value.length >= 8;
mark(email, okEmail);
mark(pw, okPw);
return okEmail && okPw ? "" : "メールと8文字以上のパスワードを入力してください";
},
() => {
const name = track.elements.name;
const okName = name.value.trim().length > 0;
mark(name, okName);
return okName ? "" : "ワークスペース名を入力してください";
},
() => "" // 確認ステップは検証不要
];
// 入力欄の妥当性を見た目に反映
function mark(input, ok) {
if (input) input.classList.toggle("invalid", !ok);
}
// 表示中ステップへスライド&インジケータ更新
function render() {
track.style.transform = `translateX(-${current * 33.3333}%)`;
steps.forEach((s, i) => {
s.classList.toggle("is-active", i === current);
s.classList.toggle("is-done", i < current);
});
prevBtn.disabled = current === 0;
nextBtn.textContent = current === TOTAL - 1 ? "作成する" : "次へ";
setStatus("");
}
// ステータスメッセージ表示
function setStatus(msg, type = "") {
statusEl.textContent = msg;
statusEl.className = "wiz-status" + (type ? " " + type : "");
}
// 最終ステップ手前で確認サマリを生成
function buildSummary() {
const pwLen = track.elements.password.value.length;
const rows = [
["メールアドレス", track.elements.email.value.trim()],
["パスワード", "•".repeat(pwLen)],
["ワークスペース名", track.elements.name.value.trim()],
["契約プラン", track.elements.plan.value]
];
summaryEl.innerHTML = rows
.map(([k, v]) => `<div class="wiz-row"><dt>${k}</dt><dd>${escapeHtml(v)}</dd></div>`)
.join("");
}
// XSS対策の簡易エスケープ
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
);
}
// 「次へ / 作成する」
nextBtn.addEventListener("click", () => {
const error = validators[current]();
if (error) { setStatus(error, "err"); return; }
if (current < TOTAL - 1) {
current += 1;
if (current === TOTAL - 1) buildSummary(); // 確認画面へ入る直前に集計
render();
} else {
// 実送信はしない(デモ)
setStatus("ワークスペースを作成しました ✓", "ok");
nextBtn.disabled = true;
prevBtn.disabled = true;
steps.forEach((s) => s.classList.add("is-done"));
}
});
// 「戻る」
prevBtn.addEventListener("click", () => {
if (current > 0) { current -= 1; render(); }
});
// フォーム送信はすべて抑止
track.addEventListener("submit", (e) => e.preventDefault());
render();
}
コード
HTML
<div class="stage">
<div class="wiz-card">
<!-- 上部のステップ進捗インジケータ -->
<ol class="wiz-steps" id="wiz-steps">
<li class="wiz-step is-active" data-step="0">
<span class="wiz-dot">1</span>
<span class="wiz-label">アカウント</span>
</li>
<li class="wiz-step" data-step="1">
<span class="wiz-dot">2</span>
<span class="wiz-label">プロフィール</span>
</li>
<li class="wiz-step" data-step="2">
<span class="wiz-dot">3</span>
<span class="wiz-label">確認</span>
</li>
</ol>
<!-- スライドするパネル群 -->
<div class="wiz-viewport">
<form class="wiz-track" id="wiz-track" novalidate>
<!-- ステップ1: アカウント -->
<section class="wiz-panel" aria-label="アカウント">
<label class="wiz-field">
<span>メールアドレス</span>
<input type="email" name="email" placeholder="you@example.com" autocomplete="off">
</label>
<label class="wiz-field">
<span>パスワード</span>
<input type="password" name="password" placeholder="8文字以上" autocomplete="off">
</label>
</section>
<!-- ステップ2: プロフィール -->
<section class="wiz-panel" aria-label="プロフィール">
<label class="wiz-field">
<span>表示名</span>
<input type="text" name="name" placeholder="山田 太郎" autocomplete="off">
</label>
<label class="wiz-field">
<span>プラン</span>
<select name="plan">
<option value="フリー">フリー</option>
<option value="プロ">プロ</option>
<option value="チーム">チーム</option>
</select>
</label>
</section>
<!-- ステップ3: 確認サマリ -->
<section class="wiz-panel" aria-label="確認">
<p class="wiz-summary-head">入力内容の確認</p>
<dl class="wiz-summary" id="wiz-summary"></dl>
</section>
</form>
</div>
<!-- 操作ボタン -->
<div class="wiz-nav">
<button type="button" class="wiz-btn ghost" id="wiz-prev" disabled>戻る</button>
<button type="button" class="wiz-btn solid" id="wiz-next">次へ</button>
</div>
<p class="wiz-status" id="wiz-status" role="status"></p>
</div>
</div>
CSS
:root {
/* テーマ用CSS変数 */
--accent: #6366f1;
--accent2: #8b5cf6;
--line: #e2e8f0;
--ink: #1e293b;
--muted: #64748b;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%);
color: var(--ink);
}
.stage { width: 100%; padding: 20px; display: grid; place-items: center; }
.wiz-card {
width: min(420px, 94vw);
padding: 22px 22px 18px;
background: #fff;
border-radius: 18px;
box-shadow: 0 24px 60px -26px rgba(30, 41, 59, 0.45);
}
/* 進捗インジケータ */
.wiz-steps {
list-style: none;
display: flex;
justify-content: space-between;
margin: 0 0 20px;
padding: 0;
position: relative;
}
.wiz-steps::before {
/* 全ステップを貫く下地ライン */
content: "";
position: absolute;
top: 14px; left: 14px; right: 14px;
height: 2px;
background: var(--line);
}
.wiz-step {
position: relative;
z-index: 1;
display: grid;
justify-items: center;
gap: 6px;
flex: 1;
}
.wiz-dot {
width: 28px; height: 28px;
display: grid; place-items: center;
font-size: 0.82rem;
font-weight: 700;
color: var(--muted);
background: #fff;
border: 2px solid var(--line);
border-radius: 50%;
transition: all 0.25s ease;
}
.wiz-label { font-size: 0.72rem; color: var(--muted); font-weight: 600; transition: color 0.25s ease; }
.wiz-step.is-active .wiz-dot {
color: #fff;
border-color: transparent;
background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 8px 18px -8px rgba(99, 102, 241, 0.8);
}
.wiz-step.is-active .wiz-label { color: var(--ink); }
.wiz-step.is-done .wiz-dot {
color: #fff;
border-color: transparent;
background: #34d399;
}
.wiz-step.is-done .wiz-dot::after { content: "✓"; } /* 完了は記号で表示 */
.wiz-step.is-done .wiz-dot { font-size: 0; }
.wiz-step.is-done .wiz-dot::after { font-size: 0.9rem; }
/* スライド領域 */
.wiz-viewport { overflow: hidden; }
.wiz-track {
display: flex;
width: 300%;
transform: translateX(0);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.wiz-panel {
width: 33.3333%;
flex: 0 0 33.3333%;
padding: 4px 2px;
display: grid;
gap: 12px;
align-content: start;
min-height: 152px;
}
.wiz-field { display: grid; gap: 5px; }
.wiz-field span { font-size: 0.76rem; font-weight: 600; color: var(--muted); }
.wiz-field input,
.wiz-field select {
width: 100%;
padding: 10px 12px;
font-size: 0.9rem;
color: var(--ink);
background: #f8fafc;
border: 2px solid var(--line);
border-radius: 10px;
outline: none;
transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}
.wiz-field input:focus,
.wiz-field select:focus {
border-color: var(--accent);
background: #fff;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.14);
}
.wiz-field input.invalid { border-color: #f87171; background: #fef2f2; }
/* 確認サマリ */
.wiz-summary-head { margin: 2px 0 4px; font-size: 0.9rem; font-weight: 700; color: var(--ink); }
.wiz-summary { margin: 0; display: grid; gap: 8px; }
.wiz-row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 12px; background: #f1f5f9; border-radius: 10px; }
.wiz-row dt { margin: 0; font-size: 0.76rem; color: var(--muted); font-weight: 600; }
.wiz-row dd { margin: 0; font-size: 0.84rem; color: var(--ink); font-weight: 600; text-align: right; word-break: break-all; }
/* ナビゲーション */
.wiz-nav { display: flex; gap: 10px; margin-top: 16px; }
.wiz-btn {
flex: 1;
padding: 11px 14px;
font-size: 0.88rem;
font-weight: 700;
border: none;
border-radius: 11px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.16s ease, opacity 0.16s ease;
}
.wiz-btn.solid {
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 12px 22px -12px rgba(99, 102, 241, 0.85);
}
.wiz-btn.ghost { color: var(--muted); background: #f1f5f9; }
.wiz-btn:not(:disabled):hover { transform: translateY(-1px); }
.wiz-btn:not(:disabled):active { transform: translateY(0); }
.wiz-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.wiz-status { margin: 10px 0 0; min-height: 16px; text-align: center; font-size: 0.78rem; font-weight: 600; color: var(--accent); }
.wiz-status.ok { color: #059669; }
.wiz-status.err { color: #dc2626; }
@media (prefers-reduced-motion: reduce) {
.wiz-track, .wiz-dot, .wiz-btn, .wiz-field input, .wiz-field select { transition: none; }
}
JavaScript
// 要素取得(null安全)
const track = document.getElementById("wiz-track");
const stepsEl = document.getElementById("wiz-steps");
const prevBtn = document.getElementById("wiz-prev");
const nextBtn = document.getElementById("wiz-next");
const statusEl = document.getElementById("wiz-status");
const summaryEl = document.getElementById("wiz-summary");
if (track && stepsEl && prevBtn && nextBtn) {
const steps = [...stepsEl.querySelectorAll(".wiz-step")];
const TOTAL = steps.length; // 全3ステップ
let current = 0;
// 各ステップの必須チェック定義
const validators = [
() => {
const email = track.elements.email;
const pw = track.elements.password;
const okEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
const okPw = pw.value.length >= 8;
mark(email, okEmail);
mark(pw, okPw);
return okEmail && okPw ? "" : "メールと8文字以上のパスワードを入力してください";
},
() => {
const name = track.elements.name;
const okName = name.value.trim().length > 0;
mark(name, okName);
return okName ? "" : "表示名を入力してください";
},
() => "" // 確認ステップは検証不要
];
// 入力欄の妥当性を見た目に反映
function mark(input, ok) {
if (input) input.classList.toggle("invalid", !ok);
}
// 表示中ステップへスライド&インジケータ更新
function render() {
track.style.transform = `translateX(-${current * 33.3333}%)`;
steps.forEach((s, i) => {
s.classList.toggle("is-active", i === current);
s.classList.toggle("is-done", i < current);
});
prevBtn.disabled = current === 0;
nextBtn.textContent = current === TOTAL - 1 ? "登録する" : "次へ";
setStatus("");
}
// ステータスメッセージ表示
function setStatus(msg, type = "") {
statusEl.textContent = msg;
statusEl.className = "wiz-status" + (type ? " " + type : "");
}
// 最終ステップ手前で確認サマリを生成
function buildSummary() {
const pwLen = track.elements.password.value.length;
const rows = [
["メールアドレス", track.elements.email.value.trim()],
["パスワード", "•".repeat(pwLen)],
["表示名", track.elements.name.value.trim()],
["プラン", track.elements.plan.value]
];
summaryEl.innerHTML = rows
.map(([k, v]) => `<div class="wiz-row"><dt>${k}</dt><dd>${escapeHtml(v)}</dd></div>`)
.join("");
}
// XSS対策の簡易エスケープ
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
);
}
// 「次へ / 登録する」
nextBtn.addEventListener("click", () => {
const error = validators[current]();
if (error) { setStatus(error, "err"); return; }
if (current < TOTAL - 1) {
current += 1;
if (current === TOTAL - 1) buildSummary(); // 確認画面へ入る直前に集計
render();
} else {
// 実送信はしない(デモ)
setStatus("登録が完了しました ✓", "ok");
nextBtn.disabled = true;
prevBtn.disabled = true;
steps.forEach((s) => s.classList.add("is-done"));
}
});
// 「戻る」
prevBtn.addEventListener("click", () => {
if (current > 0) { current -= 1; render(); }
});
// フォーム送信はすべて抑止
track.addEventListener("submit", (e) => e.preventDefault());
render();
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「マルチステップ・ウィザード」の効果を追加してください。
# 追加してほしい効果
マルチステップ・ウィザード(フォーム & 入力)
アカウント→プロフィール→確認の3ステップに分けた入力フォーム。上部の進捗インジケータと次へ/戻るボタンでスライド遷移し、最後に入力内容をまとめて確認できます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
<div class="wiz-card">
<!-- 上部のステップ進捗インジケータ -->
<ol class="wiz-steps" id="wiz-steps">
<li class="wiz-step is-active" data-step="0">
<span class="wiz-dot">1</span>
<span class="wiz-label">アカウント</span>
</li>
<li class="wiz-step" data-step="1">
<span class="wiz-dot">2</span>
<span class="wiz-label">プロフィール</span>
</li>
<li class="wiz-step" data-step="2">
<span class="wiz-dot">3</span>
<span class="wiz-label">確認</span>
</li>
</ol>
<!-- スライドするパネル群 -->
<div class="wiz-viewport">
<form class="wiz-track" id="wiz-track" novalidate>
<!-- ステップ1: アカウント -->
<section class="wiz-panel" aria-label="アカウント">
<label class="wiz-field">
<span>メールアドレス</span>
<input type="email" name="email" placeholder="you@example.com" autocomplete="off">
</label>
<label class="wiz-field">
<span>パスワード</span>
<input type="password" name="password" placeholder="8文字以上" autocomplete="off">
</label>
</section>
<!-- ステップ2: プロフィール -->
<section class="wiz-panel" aria-label="プロフィール">
<label class="wiz-field">
<span>表示名</span>
<input type="text" name="name" placeholder="山田 太郎" autocomplete="off">
</label>
<label class="wiz-field">
<span>プラン</span>
<select name="plan">
<option value="フリー">フリー</option>
<option value="プロ">プロ</option>
<option value="チーム">チーム</option>
</select>
</label>
</section>
<!-- ステップ3: 確認サマリ -->
<section class="wiz-panel" aria-label="確認">
<p class="wiz-summary-head">入力内容の確認</p>
<dl class="wiz-summary" id="wiz-summary"></dl>
</section>
</form>
</div>
<!-- 操作ボタン -->
<div class="wiz-nav">
<button type="button" class="wiz-btn ghost" id="wiz-prev" disabled>戻る</button>
<button type="button" class="wiz-btn solid" id="wiz-next">次へ</button>
</div>
<p class="wiz-status" id="wiz-status" role="status"></p>
</div>
</div>
【CSS】
:root {
/* テーマ用CSS変数 */
--accent: #6366f1;
--accent2: #8b5cf6;
--line: #e2e8f0;
--ink: #1e293b;
--muted: #64748b;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%);
color: var(--ink);
}
.stage { width: 100%; padding: 20px; display: grid; place-items: center; }
.wiz-card {
width: min(420px, 94vw);
padding: 22px 22px 18px;
background: #fff;
border-radius: 18px;
box-shadow: 0 24px 60px -26px rgba(30, 41, 59, 0.45);
}
/* 進捗インジケータ */
.wiz-steps {
list-style: none;
display: flex;
justify-content: space-between;
margin: 0 0 20px;
padding: 0;
position: relative;
}
.wiz-steps::before {
/* 全ステップを貫く下地ライン */
content: "";
position: absolute;
top: 14px; left: 14px; right: 14px;
height: 2px;
background: var(--line);
}
.wiz-step {
position: relative;
z-index: 1;
display: grid;
justify-items: center;
gap: 6px;
flex: 1;
}
.wiz-dot {
width: 28px; height: 28px;
display: grid; place-items: center;
font-size: 0.82rem;
font-weight: 700;
color: var(--muted);
background: #fff;
border: 2px solid var(--line);
border-radius: 50%;
transition: all 0.25s ease;
}
.wiz-label { font-size: 0.72rem; color: var(--muted); font-weight: 600; transition: color 0.25s ease; }
.wiz-step.is-active .wiz-dot {
color: #fff;
border-color: transparent;
background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 8px 18px -8px rgba(99, 102, 241, 0.8);
}
.wiz-step.is-active .wiz-label { color: var(--ink); }
.wiz-step.is-done .wiz-dot {
color: #fff;
border-color: transparent;
background: #34d399;
}
.wiz-step.is-done .wiz-dot::after { content: "✓"; } /* 完了は記号で表示 */
.wiz-step.is-done .wiz-dot { font-size: 0; }
.wiz-step.is-done .wiz-dot::after { font-size: 0.9rem; }
/* スライド領域 */
.wiz-viewport { overflow: hidden; }
.wiz-track {
display: flex;
width: 300%;
transform: translateX(0);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.wiz-panel {
width: 33.3333%;
flex: 0 0 33.3333%;
padding: 4px 2px;
display: grid;
gap: 12px;
align-content: start;
min-height: 152px;
}
.wiz-field { display: grid; gap: 5px; }
.wiz-field span { font-size: 0.76rem; font-weight: 600; color: var(--muted); }
.wiz-field input,
.wiz-field select {
width: 100%;
padding: 10px 12px;
font-size: 0.9rem;
color: var(--ink);
background: #f8fafc;
border: 2px solid var(--line);
border-radius: 10px;
outline: none;
transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}
.wiz-field input:focus,
.wiz-field select:focus {
border-color: var(--accent);
background: #fff;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.14);
}
.wiz-field input.invalid { border-color: #f87171; background: #fef2f2; }
/* 確認サマリ */
.wiz-summary-head { margin: 2px 0 4px; font-size: 0.9rem; font-weight: 700; color: var(--ink); }
.wiz-summary { margin: 0; display: grid; gap: 8px; }
.wiz-row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 12px; background: #f1f5f9; border-radius: 10px; }
.wiz-row dt { margin: 0; font-size: 0.76rem; color: var(--muted); font-weight: 600; }
.wiz-row dd { margin: 0; font-size: 0.84rem; color: var(--ink); font-weight: 600; text-align: right; word-break: break-all; }
/* ナビゲーション */
.wiz-nav { display: flex; gap: 10px; margin-top: 16px; }
.wiz-btn {
flex: 1;
padding: 11px 14px;
font-size: 0.88rem;
font-weight: 700;
border: none;
border-radius: 11px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.16s ease, opacity 0.16s ease;
}
.wiz-btn.solid {
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 12px 22px -12px rgba(99, 102, 241, 0.85);
}
.wiz-btn.ghost { color: var(--muted); background: #f1f5f9; }
.wiz-btn:not(:disabled):hover { transform: translateY(-1px); }
.wiz-btn:not(:disabled):active { transform: translateY(0); }
.wiz-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.wiz-status { margin: 10px 0 0; min-height: 16px; text-align: center; font-size: 0.78rem; font-weight: 600; color: var(--accent); }
.wiz-status.ok { color: #059669; }
.wiz-status.err { color: #dc2626; }
@media (prefers-reduced-motion: reduce) {
.wiz-track, .wiz-dot, .wiz-btn, .wiz-field input, .wiz-field select { transition: none; }
}
【JavaScript】
// 要素取得(null安全)
const track = document.getElementById("wiz-track");
const stepsEl = document.getElementById("wiz-steps");
const prevBtn = document.getElementById("wiz-prev");
const nextBtn = document.getElementById("wiz-next");
const statusEl = document.getElementById("wiz-status");
const summaryEl = document.getElementById("wiz-summary");
if (track && stepsEl && prevBtn && nextBtn) {
const steps = [...stepsEl.querySelectorAll(".wiz-step")];
const TOTAL = steps.length; // 全3ステップ
let current = 0;
// 各ステップの必須チェック定義
const validators = [
() => {
const email = track.elements.email;
const pw = track.elements.password;
const okEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
const okPw = pw.value.length >= 8;
mark(email, okEmail);
mark(pw, okPw);
return okEmail && okPw ? "" : "メールと8文字以上のパスワードを入力してください";
},
() => {
const name = track.elements.name;
const okName = name.value.trim().length > 0;
mark(name, okName);
return okName ? "" : "表示名を入力してください";
},
() => "" // 確認ステップは検証不要
];
// 入力欄の妥当性を見た目に反映
function mark(input, ok) {
if (input) input.classList.toggle("invalid", !ok);
}
// 表示中ステップへスライド&インジケータ更新
function render() {
track.style.transform = `translateX(-${current * 33.3333}%)`;
steps.forEach((s, i) => {
s.classList.toggle("is-active", i === current);
s.classList.toggle("is-done", i < current);
});
prevBtn.disabled = current === 0;
nextBtn.textContent = current === TOTAL - 1 ? "登録する" : "次へ";
setStatus("");
}
// ステータスメッセージ表示
function setStatus(msg, type = "") {
statusEl.textContent = msg;
statusEl.className = "wiz-status" + (type ? " " + type : "");
}
// 最終ステップ手前で確認サマリを生成
function buildSummary() {
const pwLen = track.elements.password.value.length;
const rows = [
["メールアドレス", track.elements.email.value.trim()],
["パスワード", "•".repeat(pwLen)],
["表示名", track.elements.name.value.trim()],
["プラン", track.elements.plan.value]
];
summaryEl.innerHTML = rows
.map(([k, v]) => `<div class="wiz-row"><dt>${k}</dt><dd>${escapeHtml(v)}</dd></div>`)
.join("");
}
// XSS対策の簡易エスケープ
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
);
}
// 「次へ / 登録する」
nextBtn.addEventListener("click", () => {
const error = validators[current]();
if (error) { setStatus(error, "err"); return; }
if (current < TOTAL - 1) {
current += 1;
if (current === TOTAL - 1) buildSummary(); // 確認画面へ入る直前に集計
render();
} else {
// 実送信はしない(デモ)
setStatus("登録が完了しました ✓", "ok");
nextBtn.disabled = true;
prevBtn.disabled = true;
steps.forEach((s) => s.classList.add("is-done"));
}
});
// 「戻る」
prevBtn.addEventListener("click", () => {
if (current > 0) { current -= 1; render(); }
});
// フォーム送信はすべて抑止
track.addEventListener("submit", (e) => e.preventDefault());
render();
}
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。