JavaScriptで作るミニアプリ4選|ToDo・電卓・クイズ・ストップウォッチ


JavaScriptの基礎文法を覚えても「で、結局なにを作れるの?」で止まってしまう人は多いはずです。JavaScriptだけ(フレームワークなし)で作れる小さなアプリは、ToDoリスト・電卓・クイズ・ストップウォッチなど、数十行のコードで完成するものがたくさんあります。手を動かして1つ作りきると、バラバラだった文法知識が一本の線でつながります。

本記事では、バニラJSで作るミニアプリを4つ、HTML・CSS・JavaScriptのコード付きで、ステップを追って作ります。基礎文法がまだの方は JavaScript練習問題23選(基礎文法編) から始めてください。各アプリには「発展・次の一手」も付けたので、作って終わりではなく自分で改造するところまで進めます。


差分チェックツールで効率UPお手本コードと自分のコードを比較して、違いを一目で確認できます。練習前にブックマークしておくと便利です。
Diff Checkerを開く →

JavaScript練習問題シリーズ

テーマ別の練習問題はこちら。基礎を固めてから、本記事のアプリ制作に進むのがおすすめです。


この記事の対象と進め方

対象は、変数・関数・配列・条件分岐とDOM操作の基礎を理解した、初級〜中級の学習者です。次の手順で進めると、コピペで終わらず「自分で書ける」状態に近づきます。

  1. 完成イメージと「使う知識」を読み、何が必要かを把握する
  2. HTML・CSSを用意し、JavaScriptを上から順に写経して動かす
  3. 一度動いたら、解答を閉じて「発展・次の一手」を自力で実装する

コードは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 で復元します。formsubmit では 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.20.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 / clearInterval
  • Date.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())の差分から計算するのがコツです。timerIdnull かどうかで起動状態を判定し、二重起動を防いでいます。ストップ時は経過を 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学習の全体像を確認したい場合は、学習ガイドが入り口になります。


実装・制作のご相談

「作りたいアプリやサービスのアイデアはあるけれど、実装まで手が回らない」——そんなときは、Web制作・開発のプロに任せるのも選択肢です。フロントエンドの実装やアプリ開発のご相談は、制作チーム RINIA で承っています。