JavaScriptの基礎文法を覚えても「で、結局なにを作れるの?」で止まってしまう人は多いはずです。JavaScriptだけ(フレームワークなし)で作れる小さなアプリは、ToDoリスト・電卓・クイズ・ストップウォッチなど、数十行のコードで完成するものがたくさんあります。手を動かして1つ作りきると、バラバラだった文法知識が一本の線でつながります。
本記事では、バニラJSで作るミニアプリを4つ、HTML・CSS・JavaScriptのコード付きで、ステップを追って作ります。基礎文法がまだの方は JavaScript練習問題23選(基礎文法編) から始めてください。各アプリには「発展・次の一手」も付けたので、作って終わりではなく自分で改造するところまで進めます。
JavaScript練習問題シリーズ
テーマ別の練習問題はこちら。基礎を固めてから、本記事のアプリ制作に進むのがおすすめです。
- 基礎文法 23問
- DOM操作 10問
- 配列メソッド 10問
- 非同期処理 10問
- バグ修正・デバッグ 12問
- ミニアプリ制作 4選(この記事)
この記事の対象と進め方
対象は、変数・関数・配列・条件分岐とDOM操作の基礎を理解した、初級〜中級の学習者です。次の手順で進めると、コピペで終わらず「自分で書ける」状態に近づきます。
- 完成イメージと「使う知識」を読み、何が必要かを把握する
- HTML・CSSを用意し、JavaScriptを上から順に写経して動かす
- 一度動いたら、解答を閉じて「発展・次の一手」を自力で実装する
コードはHTMLファイルに <script> として書くか、CodePen などのオンラインエディタに貼れば、その場で動かせます。
① ToDoリスト(localStorageで保存)
やることを追加・完了・削除でき、ブラウザを閉じても残るToDoアプリです。状態管理とデータの永続化(localStorage)という、実務アプリの土台になる考え方が一通り体験できます。
使う知識
addEventListenerによるイベント処理- DOMの動的生成(
createElement/appendChild) - 配列の
push/filter - localStorage と
JSON.stringify/parse
HTML
<div class="todo-app">
<form id="todo-form">
<input id="todo-input" type="text" placeholder="やることを入力" />
<button type="submit">追加</button>
</form>
<ul id="todo-list"></ul>
</div>CSS(最小限)
.todo-app { max-width: 400px; margin: 0 auto; }
#todo-list li { display: flex; justify-content: space-between; padding: 8px; border-bottom: 1px solid #ddd; }
#todo-list li.done span { text-decoration: line-through; color: #999; }JavaScript
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const list = document.getElementById('todo-list');
// localStorageから読み込み(なければ空配列)
let todos = JSON.parse(localStorage.getItem('todos')) || [];
function save() {
localStorage.setItem('todos', JSON.stringify(todos));
}
function render() {
list.innerHTML = '';
todos.forEach((todo, index) => {
const li = document.createElement('li');
if (todo.done) li.classList.add('done');
const span = document.createElement('span');
span.textContent = todo.text;
// クリックで完了状態を切り替え
span.addEventListener('click', () => {
todos[index].done = !todos[index].done;
save();
render();
});
const del = document.createElement('button');
del.textContent = '削除';
del.addEventListener('click', () => {
todos = todos.filter((_, i) => i !== index);
save();
render();
});
li.append(span, del);
list.appendChild(li);
});
}
form.addEventListener('submit', (e) => {
e.preventDefault(); // 送信でのリロードを防ぐ
const text = input.value.trim();
if (!text) return; // 空入力は無視
todos.push({ text, done: false });
input.value = '';
save();
render();
});
render(); // 初期表示ポイント:状態(todos配列)を「唯一の正しいデータ」とし、変更のたびに save() → render() を呼ぶ設計にすると、画面とデータが必ず一致します。localStorageは文字列しか保存できないため、JSON.stringify で保存し JSON.parse で復元します。form の submit では e.preventDefault() でリロードを止めるのを忘れないようにしましょう。
発展・次の一手:編集機能、期限の追加、ドラッグ&ドロップでの並べ替えに挑戦してみましょう。次の段階としてフレームワークに進むなら、同じToDoを Reactで作るTODOアプリ で作り直すと、状態管理の考え方の違いがよく分かります。localStorageの仕組みは Web Storageの使い分け も参考に。
② 電卓
四則演算ができるシンプルな電卓です。イベント委譲と状態管理という、UI開発で何度も使う考え方が身につきます。
使う知識
data-*属性とdataset- イベント委譲(親要素で受ける)
switchによる分岐- 状態を複数の変数で管理する
HTML
<div class="calc">
<div id="display">0</div>
<div class="buttons">
<button data-num="7">7</button>
<button data-num="8">8</button>
<button data-num="9">9</button>
<button data-op="/">/</button>
<button data-num="4">4</button>
<button data-num="5">5</button>
<button data-num="6">6</button>
<button data-op="*">*</button>
<button data-num="1">1</button>
<button data-num="2">2</button>
<button data-num="3">3</button>
<button data-op="-">-</button>
<button data-num="0">0</button>
<button data-action="clear">C</button>
<button data-action="equals">=</button>
<button data-op="+">+</button>
</div>
</div>CSS(最小限)
.calc { width: 240px; margin: 0 auto; }
#display { text-align: right; padding: 12px; background: #222; color: #fff; font-size: 24px; }
.buttons { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-top: 4px; }
.buttons button { padding: 16px; font-size: 18px; cursor: pointer; }JavaScript
const display = document.getElementById('display');
let current = '0'; // 入力中の数値(文字列)
let previous = null; // 確定済みの数値
let operator = null; // 選択中の演算子
function updateDisplay() {
display.textContent = current;
}
function inputNumber(num) {
current = current === '0' ? num : current + num;
updateDisplay();
}
function chooseOperator(op) {
if (operator && previous !== null) calculate(); // 連続演算に対応
previous = Number(current);
operator = op;
current = '0';
}
function calculate() {
if (operator === null || previous === null) return;
const a = previous;
const b = Number(current);
let result;
switch (operator) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/': result = b === 0 ? 'エラー' : a / b; break;
}
current = String(result);
previous = null;
operator = null;
updateDisplay();
}
function clearAll() {
current = '0';
previous = null;
operator = null;
updateDisplay();
}
// イベント委譲:ボタンの親に1つだけリスナーを付ける
document.querySelector('.buttons').addEventListener('click', (e) => {
const btn = e.target;
if (btn.dataset.num) {
inputNumber(btn.dataset.num);
} else if (btn.dataset.op) {
chooseOperator(btn.dataset.op);
} else if (btn.dataset.action === 'equals') {
calculate();
} else if (btn.dataset.action === 'clear') {
clearAll();
}
}); ポイント:ボタン1つずつにリスナーを付けるのではなく、親要素にまとめて付けるイベント委譲を使うと、コードが簡潔になります。どのボタンが押されたかは data-* 属性(dataset)で判別します。状態は current / previous / operator の3つで管理し、0除算は明示的にガードしています。
発展・次の一手:小数点ボタン、連続入力、キーボード対応を足してみましょう。0.1 + 0.2 が 0.3 にならない浮動小数点の問題に当たったら、バグ修正編の問5 で原因と対処を確認できます。
③ クイズアプリ(スコア集計)
3択クイズに答えると得点が集計され、最後に結果が出るアプリです。データとUIの分離という、規模が大きくなっても破綻しない設計の基本を学べます。
使う知識
- 配列とオブジェクトでのデータ管理
- DOMの動的生成
- テンプレートリテラル(
`...`) - 進行状態の管理
HTML
<div class="quiz">
<div id="quiz-box">
<p id="question"></p>
<div id="choices"></div>
</div>
<p id="result" hidden></p>
</div>CSS(最小限)
.quiz { max-width: 480px; margin: 0 auto; }
#choices button { display: block; width: 100%; padding: 12px; margin: 6px 0; cursor: pointer; }
#result { font-size: 20px; font-weight: bold; }JavaScript
const quizData = [
{
question: 'JavaScriptで変数を宣言するキーワードは?',
choices: ['var', 'int', 'string', 'def'],
answer: 0,
},
{
question: '配列の要素数を取得するプロパティは?',
choices: ['size', 'count', 'length', 'total'],
answer: 2,
},
{
question: 'オブジェクトをJSON文字列に変換する関数は?',
choices: ['JSON.parse', 'JSON.stringify', 'toString', 'String'],
answer: 1,
},
];
const questionEl = document.getElementById('question');
const choicesEl = document.getElementById('choices');
const resultEl = document.getElementById('result');
let currentIndex = 0;
let score = 0;
function showQuestion() {
const q = quizData[currentIndex];
questionEl.textContent = `Q${currentIndex + 1}. ${q.question}`;
choicesEl.innerHTML = '';
q.choices.forEach((choice, i) => {
const btn = document.createElement('button');
btn.textContent = choice;
btn.addEventListener('click', () => selectAnswer(i));
choicesEl.appendChild(btn);
});
}
function selectAnswer(index) {
if (index === quizData[currentIndex].answer) {
score++;
}
currentIndex++;
if (currentIndex < quizData.length) {
showQuestion();
} else {
showResult();
}
}
function showResult() {
document.getElementById('quiz-box').hidden = true;
resultEl.hidden = false;
resultEl.textContent = `${quizData.length}問中 ${score}問正解!`;
}
showQuestion(); // 最初の問題を表示ポイント:問題は quizData というデータ(配列+オブジェクト)にまとめ、表示ロジックと分離しています。こうしておくと、問題を増やすときは配列に要素を足すだけで済みます。currentIndex で進行を、score で得点を管理し、最後の問題を超えたら結果画面に切り替えます。
発展・次の一手:正解・不正解のフィードバック表示、選択肢のシャッフル、制限時間を足してみましょう。選択肢のシャッフルには配列操作が役立ちます(配列メソッド10問)。外部ファイルやAPIから問題を読み込むなら 非同期処理10問 が参考になります。
④ ストップウォッチ
スタート・ストップ・リセットができるストップウォッチです。タイマー処理(setInterval)と時刻計算の正しい扱い方が身につきます。
使う知識
setInterval/clearIntervalDate.now()による時刻計算padStartでの桁揃え- 起動状態のフラグ管理
HTML
<div class="stopwatch">
<div id="time">00:00.0</div>
<button id="start">スタート</button>
<button id="stop">ストップ</button>
<button id="reset">リセット</button>
</div>CSS(最小限)
.stopwatch { text-align: center; }
#time { font-size: 48px; font-variant-numeric: tabular-nums; margin-bottom: 12px; }
.stopwatch button { padding: 10px 16px; margin: 0 4px; cursor: pointer; }JavaScript
const timeEl = document.getElementById('time');
let startTime = 0; // 計測を開始した時刻
let elapsed = 0; // ストップまでに累積した経過ミリ秒
let timerId = null; // setIntervalのID(停止判定にも使う)
function format(ms) {
const totalSec = Math.floor(ms / 1000);
const min = String(Math.floor(totalSec / 60)).padStart(2, '0');
const sec = String(totalSec % 60).padStart(2, '0');
const tenths = Math.floor((ms % 1000) / 100);
return `${min}:${sec}.${tenths}`;
}
function update() {
// 表示は実時刻の差分から計算する(setInterval自体は正確な時計ではない)
timeEl.textContent = format(elapsed + (Date.now() - startTime));
}
document.getElementById('start').addEventListener('click', () => {
if (timerId !== null) return; // 二重起動を防ぐ
startTime = Date.now();
timerId = setInterval(update, 100);
});
document.getElementById('stop').addEventListener('click', () => {
if (timerId === null) return;
clearInterval(timerId);
timerId = null;
elapsed += Date.now() - startTime; // 経過を累積して保持
});
document.getElementById('reset').addEventListener('click', () => {
clearInterval(timerId);
timerId = null;
elapsed = 0;
timeEl.textContent = '00:00.0';
}); ポイント:setInterval は「正確な時計」ではなく、わずかにずれます。そこで表示の更新は setInterval に任せ、経過時間は実時刻(Date.now())の差分から計算するのがコツです。timerId が null かどうかで起動状態を判定し、二重起動を防いでいます。ストップ時は経過を elapsed に足し込んで保持します。
発展・次の一手:ラップ計測や、残り時間が0になったら通知する「カウントダウン版」に発展させてみましょう。さらに精度を上げたい場合は requestAnimationFrame を使う方法も調べてみてください。
動かないときは(デバッグの指針)
コードが思いどおりに動かないときは、まずブラウザの開発者ツール(DevTools)のコンソールを開き、赤いエラーメッセージを読みましょう。null 参照や undefined、関数名のタイプミスが大半です。
「コードは合っているはずなのに動かない」ときは、JavaScript練習問題【バグ修正編】 で、中級者がやりがちな典型バグ(var/letのスコープ、await忘れ、thisの消失など)の直し方を確認できます。
よくある質問(FAQ)
Q. プログラミング初心者でもミニアプリは作れますか?
変数・関数・配列・条件分岐とDOM操作の基礎を一通り終えていれば作れます。本記事はコードをステップに分け、各行にコメントを付けているので、写経しながら仕組みを理解できます。
Q. ReactなどのフレームワークはJavaScript学習に必要ですか?
小さなアプリはバニラJS(フレームワークなし)で十分作れます。まず素のJavaScriptでDOMや状態管理の仕組みを理解してからReactなどに進むと、フレームワークが何を自動化しているのかが分かり、習得が早くなります。
Q. 書いたコードはどこで動かせばいいですか?
HTMLファイルに <script> として書いてブラウザで開くか、CodePen・JSFiddle などのオンラインエディタに貼れば、その場で動作を確認できます。
Q. localStorageに保存したデータはどこにありますか?
ブラウザごと・サイト(オリジン)ごとに保存されます。DevToolsの「Application」タブの Local Storage から、保存内容の確認や削除ができます。シークレットウィンドウでは保持されない点に注意しましょう。
Q. 作ったアプリを公開するにはどうすればいいですか?
HTML・CSS・JSだけで動くアプリは、GitHub Pages などの静的ホスティングサービスで無料公開できます。ポートフォリオとして見せられる形になります。
Q. 次は何を作ればスキルが伸びますか?
各アプリの「発展・次の一手」をまず実装するのが近道です。慣れたら、興味のある題材(家計簿、タイピングゲーム、天気表示など)を、ここで学んだ状態管理・DOM生成・データ分離の型に当てはめて作ってみましょう。
関連記事と次のステップ
アプリを作る土台になる文法やテーマは、シリーズの各記事で個別に練習できます。
- JavaScript練習問題23選(基礎文法編)
- JavaScript練習問題(中級・バグ修正編)
- JavaScript配列メソッド練習問題10選
- JavaScript非同期処理の練習問題10選
- JavaScript DOM操作の練習問題10選
- Reactで作るTODOアプリ(ドラッグ&ドロップ)
JavaScript学習の全体像を確認したい場合は、学習ガイドが入り口になります。
実装・制作のご相談
「作りたいアプリやサービスのアイデアはあるけれど、実装まで手が回らない」——そんなときは、Web制作・開発のプロに任せるのも選択肢です。フロントエンドの実装やアプリ開発のご相談は、制作チーム RINIA で承っています。
