4択クイズサイト作成ガイド:基礎から実践的なテンプレート構築まで
本レポートは、インタラクティブな4択クイズサイトの構築を目指す開発者向けに、包括的なテンプレートと詳細な解説を提供するものです。基本的なHTML、CSS、JavaScriptの知識を持つ学習者が、単なるコードの断片ではなく、堅牢で拡張性の高いウェブアプリケーションの設計思想を理解し、実践的なスキルを習得することを目的としています。シンプルな構造から始め、段階的に高度な機能を追加していくことで、プロフェッショナルな品質のクイズアプリケーションを完成させます。
第1章 基盤となる構造:HTMLスケルトンの構築
ウェブアプリケーション開発の第一歩は、その骨格となるHTML構造を設計することです。ここでは、現代的なウェブ開発のプラクティスに則り、ユーザー体験を向上させるためのアーキテクチャを採用します。具体的には、複数のHTMLページを読み込むのではなく、単一のページ内でコンテンツを切り替える「シングルページアプリケーション(SPA)」のアプローチを取り入れます。
シングルページアプリケーション(SPA)アプローチの概念
SPAは、ユーザーがアプリケーションを操作する際に、ページ全体を再読み込みすることなく、必要な部分だけを動的に更新する設計です。これにより、デスクトップアプリケーションのような高速で滑らかなユーザー体験が実現されます 。
このクイズアプリケーションでは、「スタート画面」「クイズ画面」「結果画面」という3つの主要なビューを、それぞれ独立したdivコンテナとして単一のHTMLファイル内に配置します。そして、JavaScriptを用いてこれらのコンテナの表示・非表示を切り替えることで、画面遷移をシミュレートします。
このアーキテクチャを選択する理由は、単に見た目が滑らかになるからだけではありません。より重要なのは、アプリケーションの状態管理が劇的に簡素化される点です。例えば、ユーザーのスコアや現在の問題番号といったデータは、ページ遷移のたびに失われることなく、JavaScriptの変数内で一貫して保持できます。従来の複数ページ構成でこれを実現しようとすると、ローカルストレージやURLパラメータといった、より複雑な技術を介して状態を受け渡す必要が生じます。SPAアプローチは、アプリケーションのロジックをシンプルに保ち、開発と保守を容易にするための戦略的な選択なのです。
HTMLファイル (index.html) の構造
以下に、SPAアプローチに基づいたindex.htmlの完全な構造を示します。各要素には、その役割を明確にするためのIDが付与されています。
<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>4択クイズ</title>
<link rel=”stylesheet” href=”style.css”>
</head>
<body>
<main class=”app-container”>
<div id=”start-screen”>
<h1>ようこそ!4択クイズへ</h1>
<p>準備ができたら下のボタンを押してクイズを開始してください。</p>
<button id=”start-button”>クイズ開始</button>
</div>
<div id=”quiz-screen” class=”hidden”>
<div id=”hud”>
<div id=”hud-item”>
<p class=”hud-prefix”>問題</p>
<h2 id=”question-counter” class=”hud-main-text”></h2>
</div>
<div id=”hud-item”>
<p class=”hud-prefix”>スコア</p>
<h2 id=”score” class=”hud-main-text”>0</h2>
</div>
</div>
<h2 id=”question-text”>ここに問題文が表示されます。</h2>
<div id=”choices-container”>
</div>
</div>
<div id=”end-screen” class=”hidden”>
<h2>クイズ終了!</h2>
<p>あなたの最終スコアは…</p>
<h2 id=”final-score”>0</h2>
<button id=”restart-button”>もう一度挑戦する</button>
</div>
</main>
<script src=”script.js” defer></script>
</body>
</html>
この構造の主要なポイントは以下の通りです。
* ボイラープレートとメタデータ: 標準的な<!DOCTYPE html>宣言、文字コードとビューポートを設定する<meta>タグ、ページのタイトル、そしてCSSファイルへのリンクが含まれます 。
* メインコンテナ: アプリケーション全体をラップする<main>タグを配置し、意味的な構造を明確にします。
* 各画面のコンテナ: #start-screen、#quiz-screen、#end-screenというIDを持つ3つのdivが、アプリケーションの各状態に対応します。
* 表示制御クラス: #quiz-screenと#end-screenには初期状態でclass=”hidden”が付与されています。このクラスはCSSでdisplay: none;と定義され、要素を非表示にします。JavaScriptがこのクラスを付け外しすることで、画面の切り替えを実現します。
* 動的コンテンツ領域: #question-textや#choices-containerのように、中身が空の要素が用意されています。これらの要素には、JavaScriptがクイズの進行に合わせて動的にコンテンツを挿入します 。これにより、HTMLは純粋な「骨格」としての役割に徹し、具体的な内容はデータとロジックに委ねられます。
* スクリプトの読み込み: <script>タグは</body>の直前に配置されています 。また、defer属性を付与することで、HTML文書の解析が完了してからスクリプトが実行されることを保証します。これにより、JavaScriptがDOM要素にアクセスしようとした際に、まだ要素が描画されていないために発生するエラーを防ぎます。
このHTML構造は、アプリケーションのロジックとプレゼンテーションを分離するための第一歩です。静的な骨格を定義し、その操作をCSSとJavaScriptに委ねることで、柔軟で保守性の高いアプリケーションの基盤を築きます。
第2章 ビジュアルデザインとユーザー体験:CSSによるスタイリング
CSSの役割は、単にウェブページを装飾することだけではありません。優れたCSSは、ユーザーインターフェースを直感的で使いやすいものにし、アプリケーションの状態を視覚的に伝える重要な役割を担います。このセクションでは、クイズアプリケーションに魅力的でレスポンシブなデザインを適用し、JavaScriptと連携して動的なフィードバックを提供するためのCSSを構築します。
CSSファイル (style.css) の構造
style.cssは、アプリケーション全体のルックアンドフィールを定義します。その構成は以下のようになります。
* 全般的なスタイリングとリセット:
ブラウザ間の表示の差異をなくすため、基本的なCSSリセットを適用します。また、body要素に基本的なフォントファミリー、背景色、テキストの色などを設定します 。アプリケーションのコンテナを画面中央に配置するために、Flexboxを使用するのが現代的な手法です 。
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
color: #333;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.app-container {
width: 100%;
max-width: 800px;
padding: 20px;
}
“`
* レイアウトと表示制御:
各画面コンテナの基本的なスタイルと、画面切り替えの鍵となる.hiddenクラスを定義します。
#start-screen, #quiz-screen, #end-screen {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.hidden {
display: none;
}
“`
* ボタンのスタイリング(インタラクションの中核):
ユーザーが直接操作するボタンは、UIデザインにおいて最も重要な要素の一つです。ここでは、視覚的なフィードバックを明確に伝えるためのスタイルを定義します 。
.btn {
padding: 15px 25px;
font-size: 1.2rem;
border: 2px solid #3498db;
background-color: white;
color: #3498db;
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
margin-top: 20px;
}
.btn:hover {
background-color: #3498db;
color: white;
}
#choices-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
width: 100%;
margin-top: 20px;
}
.choice-btn {
width: 100%;
padding: 15px;
font-size: 1rem;
text-align: left;
border: 1px solid #ccc;
background-color: #fff;
cursor: pointer;
transition: transform 0.1s;
}
.choice-btn:hover {
transform: scale(1.02);
}
“`
* 動的なフィードバックのためのスタイリング:
ユーザーの回答が正解か不正解かを即座に視覚的に伝えるためのクラスを定義します。これらのクラスは、JavaScriptによって動的にボタンに追加されます 。
.choice-btn.correct {
background-color: #2ecc71; /* 緑色 */
color: white;
border-color: #27ae60;
}
.choice-btn.incorrect {
background-color: #e74c3c; /* 赤色 */
color: white;
border-color: #c0392b;
}
.choice-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}
“`
disabled擬似クラスのスタイルは、ユーザーが一度回答を選択した後、他の選択肢を再度クリックできないようにするために重要です 。
* レスポンシブデザイン:
メディアクエリを使用して、スマートフォンなどの小さい画面でもクイズが快適にプレイできるようにレイアウトを調整します 。例えば、画面幅が狭い場合は、選択肢のグリッドレイアウトを2列から1列に変更します。
@media (max-width: 600px) {
#choices-container {
grid-template-columns: 1fr;
}
h1 {
font-size: 1.8rem;
}
h2 {
font-size: 1.4rem;
}
}
動的状態管理のための主要CSSクラス
CSSとJavaScriptが効果的に連携するためには、両者の間で「契約」を定義することが不可欠です。以下の表は、JavaScriptがUIの視覚的状態を制御するために使用する主要なCSSクラスとその役割をまとめたものです。これにより、ロジック(JavaScript)とプレゼンテーション(CSS)の分離が促進され、コードの可読性と保守性が向上します。
| クラス名 | 適用対象 | トリガー | 視覚効果 |
|—|—|—|—|
| .hidden | 各画面のdiv | JavaScript (ゲームフロー制御) | 要素を非表示にする (display: none;) |
| .correct | 選択肢ボタン (.choice-btn) | JavaScript (回答チェック) | 正解を示す (例: 緑色の背景) |
| .incorrect | 選択肢ボタン (.choice-btn) | JavaScript (回答チェック) | 不正解を示す (例: 赤色の背景) |
| :disabled | 選択肢ボタン (.choice-btn) | JavaScript (回答チェック) | 回答選択後の再クリックを防止する |
この表が示すように、CSSはもはや静的な装飾ではありません。JavaScriptが状態を変更するたびに、これらのクラスを付け外しすることで、UIがその状態を即座に反映します。このパターンは、アプリケーションの視覚的な側面と論理的な側面を明確に分離し、それぞれを独立して変更・拡張することを容易にする、現代的なフロントエンド開発の基本原則です。
第3章 クイズに命を吹き込む:コアJavaScriptロジック
HTMLで骨格を、CSSで肉付けを行った後、次はこのアプリケーションに知能と動作を与えるJavaScriptのロジックを実装します。このセクションでは、クイズの進行を管理し、ユーザーのインタラクションに応答し、UIを動的に更新する「エンジン」部分を構築します。まずは、クイズデータをスクリプト内に直接記述する、自己完結型のアプローチから始めます。
JavaScriptファイル (script.js) の初期構造
script.jsは、クイズアプリケーションのすべてのロジックを司ります。その基本的な構造は以下の通りです。
* DOM要素への参照:
スクリプトの冒頭で、HTML内の操作対象となる要素への参照をすべて取得し、変数に格納します。getElementByIdやquerySelectorを使用するこの方法は、JavaScriptでUIを操作する際の標準的なプラクティスです 。
const startScreen = document.getElementById(‘start-screen’);
const quizScreen = document.getElementById(‘quiz-screen’);
const endScreen = document.getElementById(‘end-screen’);
const startButton = document.getElementById(‘start-button’);
const restartButton = document.getElementById(‘restart-button’);
const questionText = document.getElementById(‘question-text’);
const choicesContainer = document.getElementById(‘choices-container’);
const questionCounter = document.getElementById(‘question-counter’);
const scoreDisplay = document.getElementById(‘score’);
const finalScore = document.getElementById(‘final-score’);
* 状態管理変数:
クイズの現在の状態を追跡するための変数を宣言します。これには、現在の問題のインデックスやスコアなどが含まれます 。
let questions =; // クイズデータは後で読み込む
let currentQuestionIndex = 0;
let score = 0;
* クイズデータ構造:
クイズの問題、選択肢、正解をオブジェクトの配列として定義します。このデータ構造は、柔軟性と拡張性に優れており、多くの実装例で見られる効果的な方法です 。初期段階では、この配列を直接スクリプト内に記述します。
const initialQuestions = [
{
question: “日本の首都はどこですか?”,
choices: [“大阪”, “京都”, “東京”, “名古屋”],
answer: “東京”
},
{
question: “世界で最も高い山は何ですか?”,
choices: [“K2”, “カンチェンジュンガ”, “エベレスト”, “ローツェ”],
answer: “エベレスト”
},
{
question: “JavaScriptの`const`で宣言された変数の特徴は何ですか?”,
choices: [“再代入も再宣言も可能”, “再代入は可能だが再宣言は不可能”, “再代入は不可能だが再宣言は可能”, “再代入も再宣言も不可能”],
answer: “再代入も再宣言も不可能”
}
];
中核となる関数群
クイズのロジックは、それぞれが特定の責務を持つ複数の関数によって構成されます。
* startQuiz():
この関数は、スタートボタンがクリックされたときに実行されます。クイズの初期化を担当し、スタート画面を隠してクイズ画面を表示します。スコアと問題インデックスをリセットし、最初の問題を表示するためにshowQuestion()を呼び出します。
function startQuiz() {
questions = […initialQuestions]; // ここではハードコードされたデータを使用
currentQuestionIndex = 0;
score = 0;
scoreDisplay.innerText = score;
startScreen.classList.add(‘hidden’);
endScreen.classList.add(‘hidden’);
quizScreen.classList.remove(‘hidden’);
showQuestion();
}
* showQuestion():
この関数は、UIを現在のクイズデータに基づいて描画する、データ駆動UIの中核です。
* currentQuestionIndexを基に、questions配列から現在の問題オブジェクトを取得します。
* 取得したデータでDOMを更新します。問題文をquestionTextに、問題番号をquestionCounterに設定します。
* choicesContainerの中身を一度空にしてから、現在の問題のchoices配列をループ処理し、選択肢ごとにボタンを動的に生成して追加します。
* 各選択肢ボタンにclickイベントリスナーを追加し、クリックされたらselectAnswer()関数が実行されるように設定します。
function showQuestion() {
const question = questions[currentQuestionIndex];
questionText.innerText = question.question;
questionCounter.innerText = `${currentQuestionIndex + 1} / ${questions.length}`;
choicesContainer.innerHTML = ”; // 以前の選択肢をクリア
question.choices.forEach(choice => {
const button = document.createElement(‘button’);
button.innerText = choice;
button.classList.add(‘choice-btn’);
button.addEventListener(‘click’, selectAnswer);
choicesContainer.appendChild(button);
});
}
* selectAnswer(e):
ユーザーが選択肢をクリックしたときに実行される関数です。
* イベントオブジェクトeから、クリックされたボタン(e.target)とそのテキスト(選択された回答)を取得します。
* 選択された回答が、現在の問題オブジェクトのanswerと一致するかどうかを判定します。
* 正解であればスコアを更新し、scoreDisplayの表示も更新します。
* クリックされたボタンに.correctまたは.incorrectクラスを追加して、即座に視覚的なフィードバックを与えます。
* すべての選択肢ボタンを無効化(disabled = true)し、ユーザーが再度回答することを防ぎます 。
* setTimeoutを使用して1秒程度の短い遅延を設け、ユーザーがフィードバックを確認する時間を与えた後、次の問題へ進むshowNextQuestion()を呼び出します 。
function selectAnswer(e) {
const selectedButton = e.target;
const selectedAnswer = selectedButton.innerText;
const correctAnswer = questions[currentQuestionIndex].answer;
if (selectedAnswer === correctAnswer) {
score += 10;
scoreDisplay.innerText = score;
selectedButton.classList.add(‘correct’);
} else {
selectedButton.classList.add(‘incorrect’);
}
// 全てのボタンを無効化
Array.from(choicesContainer.children).forEach(button => {
button.disabled = true;
});
setTimeout(showNextQuestion, 1000); // 1秒後に次の問題へ
}
* showNextQuestion():
currentQuestionIndexをインクリメントします。まだ表示すべき問題が残っていればshowQuestion()を再度呼び出し、そうでなければendQuiz()を呼び出してクイズを終了します。
* endQuiz():
クイズ画面を隠し、終了画面を表示します。最終スコアをfinalScore要素に表示します 。
イベントリスナーの設定
最後に、ユーザーのアクションに関数を紐付けます。
startButton.addEventListener(‘click’, startQuiz);
restartButton.addEventListener(‘click’, startQuiz); // リスタートも同じ関数を呼ぶ
データ駆動UIパラダイムの理解
このJavaScriptの構造は、単にDOMを操作しているだけではありません。これは「UIは状態の関数である」という、ReactやVueのような現代的なフレームワークの根底にある思想を、純粋なJavaScriptで具現化したものです。
showQuestion関数を例に考えてみましょう。この関数は、グローバルな状態(questions配列とcurrentQuestionIndex)を読み取り、その状態に対応するUIを描画します。UIを更新するたびに、「前の問題文を消して、新しい問題文を足す」といった命令的な手順を記述するのではなく、「現在のcurrentQuestionIndexに対応するUIをここに描画せよ」と宣言的に記述しています。
ユーザーが回答を選択し、currentQuestionIndexがインクリメントされると、状態が変化します。そして、再びshowQuestionが呼び出されると、新しい状態に基づいて新しいUIが自動的に生成されます。この「状態の変更がUIの変更を駆動する」というパターンを理解することは、単にクイズの作り方を学ぶ以上に価値があります。これは、複雑なウェブアプリケーションを、予測可能で管理しやすい方法で構築するための基本的な考え方であり、より高度なフロントエンド開発への架け橋となる重要な概念です。
第4章 高度な機能:堅牢でスケーラブルなアプリケーションへの進化
基本的なクイズが動作するようになった今、これを単なるデモからプロフェッショナルな品質のテンプレートへと昇華させます。ここでは、データの分離、タイマー機能、問題のランダム化といった高度な機能を追加し、アプリケーションの保守性、再利用性、そしてエンゲージメントを高めます。
4.1 分離の原則:JSONファイルから問題を読み込む
現在、クイズの問題はJavaScriptファイル内に直接ハードコードされています。これは小規模なデモでは問題ありませんが、アプリケーションが成長するにつれていくつかの重大な問題を引き起こします。
* 保守性の低下: 問題を追加・編集するたびにJavaScriptファイルを直接変更する必要があり、誤ってコードのロジック部分を壊してしまうリスクがあります。
* コラボレーションの困難: クイズのコンテンツを作成する非開発者(教育者やコンテンツクリエーターなど)が、安全に問題を更新することができません。
* パフォーマンス: 問題数が数百、数千になると、JavaScriptファイルが肥大化し、ページの初期読み込み時間が長くなります。
これらの問題を解決するため、クイズのデータ(問題)とアプリケーションのロジック(コード)を分離します。具体的には、問題を外部のJSONファイルに記述し、JavaScriptのfetch APIを使用して実行時に動的に読み込むように変更します 。
ステップ1: questions.jsonファイルの作成
プロジェクトのルートにquestions.jsonという名前の新しいファイルを作成し、JavaScriptの配列と同じ構造でクイズデータを記述します。
[
{
“question”: “日本の首都はどこですか?”,
“choices”: [“大阪”, “京都”, “東京”, “名古屋”],
“answer”: “東京”
},
{
“question”: “世界で最も高い山は何ですか?”,
“choices”: [“K2”, “カンチェンジュンガ”, “エベレスト”, “ローツェ”],
“answer”: “エベレスト”
},
{
“question”: “JavaScriptの`const`で宣言された変数の特徴は何ですか?”,
“choices”: [“再代入も再宣言も可能”, “再代入は可能だが再宣言は不可能”, “再代入は不可能だが再宣言は可能”, “再代入も再宣言も不可能”],
“answer”: “再代入も再宣言も不可能”
},
{
“question”: “HTMLのフルネームは何ですか?”,
“choices”:,
“answer”: “HyperText Markup Language”
}
]
ステップ2: JavaScriptのstartQuiz関数をリファクタリング
startQuiz関数をasync関数に変更し、fetch APIを使用してquestions.jsonを非同期に読み込みます。try…catchブロックを使用して、ファイルの読み込みに失敗した場合のエラーハンドリングも行います 。
async function startQuiz() {
currentQuestionIndex = 0;
score = 0;
scoreDisplay.innerText = score;
try {
const response = await fetch(‘questions.json’);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
questions = await response.json(); // JSONから読み込んだデータでquestions配列を更新
startScreen.classList.add(‘hidden’);
endScreen.classList.add(‘hidden’);
quizScreen.classList.remove(‘hidden’);
showQuestion();
} catch (error) {
console.error(“クイズデータの読み込みに失敗しました:”, error);
alert(“クイズを読み込めませんでした。ページをリロードして再試行してください。”);
}
}
このリファクタリングにより、アプリケーションのアーキテクチャは大幅に改善されます。この変更の価値をより明確に理解するために、2つのアプローチを比較してみましょう。
| 特徴 | 方法1:JSファイルへのハードコード | 方法2:外部JSONファイルからの読み込み |
|—|—|—|
| セットアップの容易さ | 非常に高い(ただ書き込むだけ) | 中程度(fetchと別ファイルが必要) |
| スケーラビリティ | 低い(JSファイルが肥大化し遅くなる) | 非常に高い(数千の問題も容易に扱える) |
| 保守性 | 低い(編集時に構文エラーのリスク) | 非常に高い(データとロジックの明確な分離) |
| 共同作業 | 困難(非開発者が安全に編集不可) | 容易(コンテンツは誰でも管理可能) |
| 最適な用途 | 素早いプロトタイプ、小規模なデモ | 本番アプリケーション、大規模なクイズ |
この表が示すように、fetchを導入するわずかな初期コストは、長期的な保守性、スケーラビリティ、そしてチームでの共同作業の容易さという、計り知れない利益によって十分に相殺されます。これは、単なる機能追加ではなく、プロフェッショナルなソフトウェア開発における重要な設計判断です。
4.2 カウントダウンタイマーの追加
クイズに時間制限を設けることで、緊張感とエンゲージメントを高めることができます。ここでは、各問題に制限時間を設定するタイマー機能を実装します 。
まず、状態変数と定数を追加します。
const TIME_LIMIT = 15; // 各問題の制限時間(秒)
let timeLeft = TIME_LIMIT;
let timerInterval = null;
次に、タイマーを管理する関数を作成します。
function startTimer() {
timeLeft = TIME_LIMIT;
// タイマー表示用のHTML要素をHUDに追加する必要がある
// <div id=”timer”>15</div>
const timerDisplay = document.getElementById(‘timer’);
timerDisplay.innerText = timeLeft;
timerInterval = setInterval(() => {
timeLeft–;
timerDisplay.innerText = timeLeft;
if (timeLeft <= 0) {
clearInterval(timerInterval);
// 時間切れの場合、不正解として扱う
handleTimeout();
}
}, 1000);
}
function resetTimer() {
clearInterval(timerInterval);
}
function handleTimeout() {
// 時間切れのフィードバック(例:背景をオレンジ色に)
document.body.style.backgroundColor = ‘#f39c12’;
Array.from(choicesContainer.children).forEach(button => {
button.disabled = true;
});
setTimeout(() => {
document.body.style.backgroundColor = ‘#f0f0f0’; // 色を元に戻す
showNextQuestion();
}, 1500);
}
これらの関数を既存のロジックに組み込みます。showQuestionの冒頭でstartTimer()を呼び出し、selectAnswerの冒頭でresetTimer()を呼び出します。これにより、新しい問題が表示されるたびにタイマーが開始され、ユーザーが回答を選択するとタイマーが停止します。
4.3 問題と選択肢のランダム化
クイズの再プレイ価値を高めるために、出題される問題の順序と、各問題の選択肢の並び順をランダム化します。これにより、ユーザーは単に答えの「位置」を記憶するだけでは正解できなくなります 。
問題のランダム化:
startQuiz関数内で、JSONから読み込んだquestions配列をシャッフルします。ここでは、シンプルで広く使われているsortメソッドとMath.randomを組み合わせた方法を使用します。
// startQuiz関数内、questions = await response.json(); の直後に追加
questions.sort(() => Math.random() – 0.5);
選択肢のランダム化:
showQuestion関数内で、各問題の選択肢を表示する前に、その問題のchoices配列をシャッフルします。正解の文字列はanswerプロパティに保持されているため、選択肢の順序が変わっても正誤判定ロジックは影響を受けません。
// showQuestion関数内、choicesContainer.innerHTML = ”; の直後に追加
const question = questions[currentQuestionIndex];
const shuffledChoices = […question.choices].sort(() => Math.random() – 0.5);
// ループ処理を shuffledChoices.forEach に変更
shuffledChoices.forEach(choice => {
//… ボタン生成ロジック…
});
これらの高度な機能を追加することで、このクイズテンプレートは、単に動作するだけでなく、ユーザーを惹きつけ、コンテンツの管理が容易で、長期的に運用可能な、堅牢なアプリケーションへと進化しました。
第5章 完成版テンプレート:組み立てとカスタマイズガイド
ここまでのステップを経て、基礎的な構造から高度な機能までを備えた、プロフェッショナルグレードの4択クイズアプリケーションが完成しました。この最終セクションでは、完成した全コードを提供し、あなたがこのテンプレートを自身のプロジェクトに合わせて簡単にカスタマイズするための詳細なガイドを示します。
完成したコードベース
以下に、このプロジェクトを構成する4つのファイル(index.html, style.css, script.js, questions.json)の最終的なコードを、詳細なコメント付きで示します。
index.html
<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>4択クイズ</title>
<link rel=”stylesheet” href=”style.css”>
</head>
<body>
<main class=”app-container”>
<div id=”start-screen”>
<h1>ようこそ!4択クイズへ</h1>
<p>準備ができたら下のボタンを押してクイズを開始してください。</p>
<button id=”start-button” class=”btn”>クイズ開始</button>
</div>
<div id=”quiz-screen” class=”hidden”>
<div id=”hud”>
<div id=”hud-item”>
<p class=”hud-prefix”>問題</p>
<h2 id=”question-counter” class=”hud-main-text”></h2>
</div>
<div id=”hud-item”>
<p class=”hud-prefix”>タイマー</p>
<h2 id=”timer” class=”hud-main-text”></h2>
</div>
<div id=”hud-item”>
<p class=”hud-prefix”>スコア</p>
<h2 id=”score” class=”hud-main-text”>0</h2>
</div>
</div>
<h2 id=”question-text”>ここに問題文が表示されます。</h2>
<div id=”choices-container”>
</div>
</div>
<div id=”end-screen” class=”hidden”>
<h2>クイズ終了!</h2>
<p>あなたの最終スコアは…</p>
<h2 id=”final-score”>0</h2>
<button id=”restart-button” class=”btn”>もう一度挑戦する</button>
</div>
</main>
<script src=”script.js” defer></script>
</body>
</html>
style.css
/* 全般的なスタイルとリセット */
body {
font-family: ‘Helvetica Neue’, Arial, ‘Hiragino Kaku Gothic ProN’, ‘Hiragino Sans’, Meiryo, sans-serif;
background-color: #f4f7f6;
color: #333;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 10px;
box-sizing: border-box;
}
.app-container {
width: 100%;
max-width: 800px;
background-color: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
h1, h2 {
color: #2c3e50;
}
/* レイアウトと表示制御 */
#start-screen, #quiz-screen, #end-screen {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.hidden {
display: none;
}
/* HUD (Heads-Up Display) スタイル */
#hud {
display: flex;
justify-content: space-around;
width: 100%;
margin-bottom: 20px;
}
#hud-item {
text-align: center;
}
.hud-prefix {
font-size: 1rem;
color: #888;
}
.hud-main-text {
font-size: 2rem;
margin: 0;
}
/* ボタンのスタイル */
.btn {
padding: 15px 30px;
font-size: 1.2rem;
border: none;
background-color: #3498db;
color: white;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, transform 0.1s;
margin-top: 20px;
}
.btn:hover {
background-color: #2980b9;
transform: translateY(-2px);
}
#choices-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
width: 100%;
margin-top: 30px;
}
.choice-btn {
width: 100%;
padding: 20px;
font-size: 1.1rem;
text-align: left;
border: 2px solid #ecf0f1;
background-color: #fff;
color: #34495e;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, border-color 0.3s, transform 0.1s;
}
.choice-btn:hover {
border-color: #3498db;
transform: scale(1.02);
}
/* 動的なフィードバック用スタイル */
.choice-btn.correct {
background-color: #2ecc71;
color: white;
border-color: #27ae60;
}
.choice-btn.incorrect {
background-color: #e74c3c;
color: white;
border-color: #c0392b;
}
.choice-btn:disabled {
cursor: not-allowed;
opacity: 0.8;
}
/* レスポンシブデザイン */
@media (max-width: 600px) {
.app-container {
padding: 20px;
}
#choices-container {
grid-template-columns: 1fr;
}
h1 {
font-size: 1.8rem;
}
h2 {
font-size: 1.4rem;
}
.btn {
padding: 12px 24px;
font-size: 1rem;
}
}
script.js
// DOM要素への参照
const startScreen = document.getElementById(‘start-screen’);
const quizScreen = document.getElementById(‘quiz-screen’);
const endScreen = document.getElementById(‘end-screen’);
const startButton = document.getElementById(‘start-button’);
const restartButton = document.getElementById(‘restart-button’);
const questionText = document.getElementById(‘question-text’);
const choicesContainer = document.getElementById(‘choices-container’);
const questionCounter = document.getElementById(‘question-counter’);
const scoreDisplay = document.getElementById(‘score’);
const finalScore = document.getElementById(‘final-score’);
const timerDisplay = document.getElementById(‘timer’);
// 状態管理変数
let questions =;
let currentQuestionIndex = 0;
let score = 0;
const TIME_LIMIT = 15; // 各問題の制限時間(秒)
let timerInterval = null;
// イベントリスナーの設定
startButton.addEventListener(‘click’, startQuiz);
restartButton.addEventListener(‘click’, startQuiz);
/**
* クイズを開始する関数
* JSONから問題を非同期に読み込み、クイズ画面を初期化する
*/
async function startQuiz() {
currentQuestionIndex = 0;
score = 0;
scoreDisplay.innerText = score;
try {
// questions.jsonからクイズデータをフェッチ
const response = await fetch(‘questions.json’);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const allQuestions = await response.json();
// 問題をランダムにシャッフル
questions = allQuestions.sort(() => Math.random() – 0.5);
// 画面を切り替え
startScreen.classList.add(‘hidden’);
endScreen.classList.add(‘hidden’);
quizScreen.classList.remove(‘hidden’);
// 最初の問題を表示
showQuestion();
} catch (error) {
console.error(“クイズデータの読み込みに失敗しました:”, error);
alert(“クイズを読み込めませんでした。ページをリロードして再試行してください。”);
}
}
/**
* 現在の問題と選択肢を表示する関数
*/
function showQuestion() {
resetTimer(); // 前の問題のタイマーをリセット
const question = questions[currentQuestionIndex];
questionText.innerText = question.question;
questionCounter.innerText = `${currentQuestionIndex + 1} / ${questions.length}`;
choicesContainer.innerHTML = ”; // 以前の選択肢をクリア
// 選択肢をシャッフルして表示
const shuffledChoices = […question.choices].sort(() => Math.random() – 0.5);
shuffledChoices.forEach(choice => {
const button = document.createElement(‘button’);
button.innerText = choice;
button.classList.add(‘choice-btn’);
button.addEventListener(‘click’, selectAnswer);
choicesContainer.appendChild(button);
});
startTimer(); // 新しいタイマーを開始
}
/**
* ユーザーが選択肢をクリックしたときの処理
* @param {Event} e – クリックイベントオブジェクト
*/
function selectAnswer(e) {
resetTimer(); // 回答が選択されたのでタイマーを停止
const selectedButton = e.target;
const selectedAnswer = selectedButton.innerText;
const correctAnswer = questions[currentQuestionIndex].answer;
// 正誤判定とスコア加算
if (selectedAnswer === correctAnswer) {
score += 10;
scoreDisplay.innerText = score;
selectedButton.classList.add(‘correct’);
} else {
selectedButton.classList.add(‘incorrect’);
}
// 全てのボタンを無効化
Array.from(choicesContainer.children).forEach(button => {
button.disabled = true;
});
// 1秒後に次の問題へ
setTimeout(showNextQuestion, 1000);
}
/**
* 次の問題へ進むか、クイズを終了するかの判定
*/
function showNextQuestion() {
currentQuestionIndex++;
if (currentQuestionIndex < questions.length) {
showQuestion();
} else {
endQuiz();
}
}
/**
* クイズを終了し、結果画面を表示する関数
*/
function endQuiz() {
quizScreen.classList.add(‘hidden’);
endScreen.classList.remove(‘hidden’);
finalScore.innerText = score;
}
/**
* タイマーを開始する関数
*/
function startTimer() {
let timeLeft = TIME_LIMIT;
timerDisplay.innerText = timeLeft;
timerInterval = setInterval(() => {
timeLeft–;
timerDisplay.innerText = timeLeft;
if (timeLeft <= 0) {
clearInterval(timerInterval);
handleTimeout();
}
}, 1000);
}
/**
* タイマーをリセット(停止)する関数
*/
function resetTimer() {
clearInterval(timerInterval);
}
/**
* 時間切れの際の処理
*/
function handleTimeout() {
// 全てのボタンを無効化し、不正解として扱う
Array.from(choicesContainer.children).forEach(button => {
button.disabled = true;
// 正解の選択肢をハイライトすることも可能
if (button.innerText === questions[currentQuestionIndex].answer) {
button.classList.add(‘correct’);
}
});
// 1.5秒後に次の問題へ
setTimeout(showNextQuestion, 1500);
}
questions.json
[
{
“question”: “日本の首都はどこですか?”,
“choices”: [“大阪”, “京都”, “東京”, “名古屋”],
“answer”: “東京”
},
{
“question”: “世界で最も高い山は何ですか?”,
“choices”: [“K2”, “カンチェンジュンガ”, “エベレスト”, “ローツェ”],
“answer”: “エベレスト”
},
{
“question”: “JavaScriptの`const`で宣言された変数の特徴は何ですか?”,
“choices”: [“再代入も再宣言も可能”, “再代入は可能だが再宣言は不可能”, “再代入は不可能だが再宣言は可能”, “再代入も再宣言も不可能”],
“answer”: “再代入も再宣言も不可能”
},
{
“question”: “CSSの正式名称は何ですか?”,
“choices”:,
“answer”: “Cascading Style Sheets”
},
{
“question”: “太陽系で最も大きい惑星は何ですか?”,
“choices”: [“地球”, “土星”, “木星”, “天王星”],
“answer”: “木星”
}
]
カスタマイズガイド
このテンプレートは、あなたのニーズに合わせて簡単に変更できるように設計されています。
* 問題の追加・変更方法:
* プロジェクト内のquestions.jsonファイルを開きます。
* 新しい問題を追加するには、既存の問題オブジェクト({…})をコピーし、配列の末尾にカンマ(,)で区切って貼り付けます。
* question(問題文)、choices(4つの選択肢の配列)、answer(正解の文字列)の値を、あなたのコンテンツに合わせて変更します。
* 注意: choices配列の中にanswerの値が必ず含まれていることを確認してください。JSONの構文(カンマや引用符)を間違えないように注意してください。
* タイマーの制限時間を調整する方法:
* script.jsファイルを開きます。
* ファイルの冒頭付近にあるconst TIME_LIMIT = 15;という行を見つけます。
* 15という数値を、あなたが設定したい秒数に変更してください。
* デザイン(ルックアンドフィール)の変更方法:
* style.cssファイルを開きます。
* 背景色、テキスト色、ボタンの色などを変更するには、対応するCSSルールを見つけてプロパティ値を変更します。
* 全体の背景色: bodyセレクタのbackground-color
* メインボタンの色: .btnセレクタのbackground-colorとcolor
* 選択肢ボタンの色: .choice-btnセレクタのbackground-colorとcolor
* 正解・不正解時の色: .correctおよび.incorrectクラスのbackground-color
* 機能の有効化・無効化:
* 問題のランダム化を無効にする: script.jsのstartQuiz関数内にあるquestions = allQuestions.sort(() => Math.random() – 0.5);という行をコメントアウト(行の先頭に//を追加)または削除します。
* 選択肢のランダム化を無効にする: script.jsのshowQuestion関数内にあるconst shuffledChoices = […question.choices].sort(() => Math.random() – 0.5);という行を削除し、その後のforEachループをquestion.choices.forEach(…)に変更します。
結論
このレポートでは、単純なHTMLの骨格から始まり、CSSによるスタイリング、JavaScriptによるコアロジックの実装、そしてJSON読み込みやタイマーといった高度な機能の追加に至るまで、4択クイズサイトの構築プロセスを段階的に解説しました。
重要なのは、完成したコードそのものだけではありません。その背後にある**「関心の分離」(HTML、CSS、JSの役割分担)、「データとロジックの分離」(JSONの活用)、そして「状態駆動UI」**といった設計思想を理解することです。これらの原則は、このクイズアプリに限らず、あらゆるモダンなウェブアプリケーション開発に応用可能な、普遍的で強力な知識です。
このテンプレートが、あなたの学習と開発の旅における確かな一歩となることを願っています。ここからさらに、スコアランキング機能、カテゴリ選択機能、解説表示機能などを追加し、あなただけのオリジナルクイズアプリケーションを創造してみてください。


コメント