クイズUI/UX改善 - オンボーディングモーダルからスケルトンUIまで
朝、初見ユーザーのつもりでクイズページを開いてみた。問題数191問、カテゴリ選択、ランダム出題。何がどう動くのか、説明がどこにもない。自分で作ったページなのに3秒ほど固まった。ここから「初めて触る人が迷わないUI」を一日かけて積み上げていった記録。
オンボーディングモーダルの設計と実装
4ステップの構成
初回訪問ユーザー向けに、4ステップのオンボーディングモーダルを設計した。
- Step 1: 問題数の紹介。全191問、無料で68問、ログインすると+123問が解放される旨を表示
- Step 2: 操作説明。実際のクイズUIに近いレイアウトで、選択肢をタップする動作を視覚的に伝える
- Step 3: 解説画面の説明。正解・不正解後に表示される解説の見方を案内
- Step 4: CTA。「さっそく始める」ボタンでモーダルを閉じてクイズ画面へ遷移
Step 2は当初テキストだけで説明していたが、実際のクイズUIに近いモックを配置する形に改善した。文字で「選択肢をタップします」と書くより、ボタンが並んでいるほうが手が動く。
localStorageによる表示制御
モーダルの表示制御に localStorage を使った。ポイントは dismissed と completed を分離したこと。
- dismissed: 「スキップ」で閉じた場合。次回訪問時に再表示する
- completed: 最後のステップまで進んだ場合。以降は表示しない
// dismissed と completed を分離して管理
const STORAGE_KEY = 'quiz-onboarding'
type OnboardingState = { dismissed: boolean; completed: boolean }
「スキップした人は興味がないから二度と出さない」という設計も考えたが、初回は操作に不慣れでスキップしてしまうケースがある。completed だけを永続的な非表示条件にした。
worktreeでの実装
オンボーディングモーダルはworktreeで別ブランチを切って実装し、完成後にmainへpushした。既存のクイズ機能に影響を与えずに試行錯誤できた。
複数回答判定の修正
複数の正解がある問題で、1つ選んだ時点で不正解と判定されるバグがあった。原因は判定ロジックが選択のたびに即座に正誤を返していたこと。
修正後は、正解数と選択数が一致するまで pending を返すようにした。ユーザーが2つ選ぶべき問題で1つだけ選んだ段階では、まだ判定しない。全て選び終わった時点で初めて正誤を確定する。
選択ボタンとカードのスタイル変更
選択ボタン
選択済みボタンの背景色を黒から薄いブルー+青ボーダーに変更した。黒背景だと「確定した」ように見えてしまい、複数選択時に追加で選ぶ気にならない。薄いブルーにしたことで「まだ選択中」という状態が視覚的に伝わる。
「全範囲」カードの背景色
カテゴリ選択画面の「全範囲」カードの背景色をグレーから白に統一した。他のカテゴリカードと同じ白背景にすることで、特別扱いされている印象を消した。
未ログイン時の表示修正
未ログイン時に「全範囲」カードが191問と表示されていたが、実際にアクセスできるのは無料の68問だけだった。未ログイン時は68問と表示するよう修正した。数字が嘘をつくとユーザーの信頼を失う。
スケルトンUI・エラー状態・空状態の実装
practice、random、reviewの3画面に、ローディング・エラー・空の3状態を実装した。
スケルトンUI
データ取得中にコンテンツ領域が空白になる問題を、スケルトンUIで解消した。カードやリストのシルエットがパルスアニメーションで表示され、「読み込み中」であることが目に見える。
エラー状態
API呼び出し失敗時に「エラーが発生しました。再読み込みしてください」というメッセージとリトライボタンを表示するようにした。
空状態
問題が0件のカテゴリを選んだ場合に「このカテゴリにはまだ問題がありません」というメッセージを表示。以前は何も表示されず、壊れているのか空なのか区別がつかなかった。
Promise.all()による並列ロード
practice画面で、カテゴリ一覧の取得と問題数の取得を直列に実行していた。Promise.all() で並列化したところ、体感でローディング時間が短くなった。
// Before: 直列
const categories = await fetchCategories()
const counts = await fetchCounts()
// After: 並列
const [categories, counts] = await Promise.all([
fetchCategories(),
fetchCounts()
])
重複CSSのグローバル化
practice、random、reviewの3画面で同じボタンスタイルやカードスタイルが重複していた。共通CSSを app.vue に移動してグローバル化した。3画面で同じ修正を3回繰り返す未来が見えたので、このタイミングで統合した。
ログインプロンプトモーダルの改善
未ログイン状態でロック問題をタップした際に表示されるモーダルのデザインを改善した。
- 不要な
<br>タグを除去し、余白をCSSで制御 - Google OAuthのアイコンをボタンに追加
- Google OAuthの認証フローが正しく動作することを確認
ProductId重複インポートの解消
複数ファイルで ProductId 型を個別に定義・インポートしていた。共通の型定義ファイルから一箇所でエクスポートする形に整理した。
データ構造の改善
クイズの選択肢を文字列で管理していたが、{account, amount} のオブジェクト形式に分離した。
// Before: 文字列結合
choices: ["現金 100,000", "売上 100,000"]
// After: 構造化
choices: [
{ account: "現金", amount: 100000 },
{ account: "売上", amount: 100000 }
]
勘定科目と金額を別フィールドに持つことで、金額のフォーマット変更や勘定科目の検索が文字列パースなしで行える。
振り返り
一日でオンボーディングモーダル、判定ロジック、3画面のスケルトンUI、CSS統合と幅広く手を入れた。どれも「初めて触る人が何に戸惑うか」を起点にした改善だった。
特にオンボーディングモーダルのStep 2を、テキスト説明から実際のUI風モックに切り替えた判断は正解だった。操作説明は「読ませる」より「見せる」方が伝わる。dismissed/completedの分離も、ユーザー行動をもう一段掘り下げて考えた結果で、この粒度の設計判断を積み重ねることが使い勝手に効いてくる。
スケルトンUIとエラー状態は、実装コストに対してユーザー体験の改善幅が大きい。「何も表示されない」状態を放置すると、ユーザーはページが壊れたと思って離脱する。ローディング・エラー・空の3状態を網羅的に実装しておくと、どんな状況でもユーザーに「今何が起きているか」を伝えられる。