第2回 試験用キッチンタイマー制作|デザイン・スマホ対応編

この記事は約28分で読めます。

前回は、Visual Studio Codeを導入し、停止・再開・リセットができる単体キッチンタイマーを作りました。

タイマーとしては動くようになりましたが、見た目は動作確認を優先した簡単なものでした。今回は、教室の大型ディスプレイで遠くから見ても残り時間が分かるように、デザインを大きく変更します。

きょろぽん
きょろぽん

前回は動くところまでできたけど、教室の後ろから見るには数字が小さいな。せっかくだから、時計を思い切り大きくして見やすくしたい。

ノン
ノン

今回は見た目を整えながら、時間の設定方法も大きな数字へまとめよう。パソコンだけでなく、スマートフォンの縦画面と横画面でも使える形にするね。

今回の目標
時計表示を画面の主役にし、大きな数字から直接時間を調整できるデザインへ変更します。さらに、画面幅に応じて大きさや配置が変わるレスポンシブ対応を行います。

今回作るもの

  • 黒を基調にした、目に優しい画面へ変更する
  • 残り時間を可能な限り大きく表示する
  • 大きな分・秒の上下に、小さな△▽ボタンを置く
  • 大きな数字を押した場合も時間を直接入力できるようにする
  • 上部の「タイマー1」を押して、タイマー名を変更できるようにする
  • スタート後は調整ボタンを隠し、残り時間を見やすくする
  • パソコン、スマートフォンの縦画面・横画面へ対応する

第2回は本来、外観とスマートフォン対応を中心にする予定でした。しかし、デザインを相談する中で、別の入力欄を置かず、大きな時計そのものから時間を設定した方が分かりやすいという案になりました。

さらに、空いた上部へ「タイマー1」と表示し、クリックして「課題3」「文書作成」「試験終了まで」など、用途に合った名前へ変更できるようにします。

最初にデザインを相談した

いきなりコードを書き始めるのではなく、まずはAIへ複数のデザイン案を画像として作ってもらいました。

どの案も黒を基調にして、大きな白い時計を中央へ配置しています。その中から、青い光を使ったガラス風のデザインを土台に選びました。

最初の案では、大きな時計の下に分と秒の入力欄がありました。しかし、同じ数字が上下に二重表示されるため、下の入力欄をなくし、大きな時計へ直接△▽を付ける案へ変更しました。

空いた場所へタイマー名を表示する案も試しました。最終的には、「キッチンタイマー」という固定タイトルをなくし、上部へ変更可能な「タイマー1」を大きく表示する形に決めました。

画像生成で作ったデザインをそのまま完成品として使うのではなく、見た目、配置、操作方法を相談するための設計図として利用しています。

AIとの共同制作について
必要な機能、使い方、画面の見せ方、修正点は私が考え、HTML・CSS・JavaScriptのコード作成をAIのノンが担当しています。私は出力されたコードを配置し、実際に動かしながら確認と修正判断を行います。

第1回のフォルダをコピーする

第1回で作成した内容を残すため、今回も新しい番号フォルダへコピーしてから修正します。

  1. Visual Studio Codeで開いているKITCHEN-TIMERフォルダを確認します。
  2. 01フォルダをコピーします。
  3. コピーしたフォルダ名を02へ変更します。
  4. 02内のindex.html、style.css、script.jsを編集します。

第1回で説明したVisual Studio Codeの起動方法やファイルの作り方は、今回は繰り返しません。分からなくなった場合は、第1回の記事へ戻って確認してください。

index.htmlを書き換える

02フォルダのindex.htmlを開き、内容をすべて次のコードへ置き換えます。

index.html のソースコードを表示
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>試験用キッチンタイマー</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="timer-page">
    <section class="timer-panel" aria-labelledby="timer-title">
      <button
        id="timer-title"
        class="timer-title"
        type="button"
        aria-label="タイマー名を変更"
      >
        タイマー1
      </button>

      <p id="error-message" class="error-message" aria-live="polite"></p>

      <div class="time-editor" aria-label="時間設定">
        <div class="time-unit">
          <button
            class="adjust-button"
            type="button"
            data-target="minutes"
            data-change="1"
            aria-label="分を1増やす"
          >▲</button>

          <button
            id="minutes-display"
            class="time-number"
            type="button"
            aria-label="分を直接入力"
          >05</button>

          <button
            class="adjust-button"
            type="button"
            data-target="minutes"
            data-change="-1"
            aria-label="分を1減らす"
          >▼</button>
        </div>

        <span class="time-colon" aria-hidden="true">:</span>

        <div class="time-unit">
          <button
            class="adjust-button"
            type="button"
            data-target="seconds"
            data-change="1"
            aria-label="秒を1増やす"
          >▲</button>

          <button
            id="seconds-display"
            class="time-number"
            type="button"
            aria-label="秒を直接入力"
          >00</button>

          <button
            class="adjust-button"
            type="button"
            data-target="seconds"
            data-change="-1"
            aria-label="秒を1減らす"
          >▼</button>
        </div>
      </div>

      <div class="buttons">
        <button id="start-button" type="button">▶ スタート</button>
        <button id="pause-button" type="button" disabled>Ⅱ 一時停止</button>
        <button id="reset-button" type="button">↻ リセット</button>
      </div>
    </section>
  </main>

  <script src="script.js"></script>
</body>
</html>

入力後、Ctrl+Sを押して保存します。

viewportを追加する

スマートフォンへ対応するため、<head>内へ次の1行を追加しています。

<meta name="viewport" content="width=device-width, initial-scale=1.0">

この指定がないと、スマートフォンでパソコン用の広い画面を縮小して表示するような状態になり、文字やボタンが小さく見える場合があります。

大きな時計を操作部分として使う

第1回では、分と秒を入力する欄と、残り時間を表示する場所が分かれていました。今回は、大きな数字自体をボタンとして使用します。

  • 05の上にある▲で分を増やす
  • 05の下にある▼で分を減らす
  • 00の上にある▲で秒を増やす
  • 00の下にある▼で秒を減らす
  • 大きな数字を押すと、数値を直接入力する

△▽は遠くから残り時間を見るときの邪魔にならないよう、小さく控えめに表示します。

style.cssを書き換える

続いて、style.cssの内容をすべて次のコードへ置き換えます。

style.css のソースコードを表示
* {
  box-sizing: border-box;
}

:root {
  color-scheme: dark;
  font-family:
    "Segoe UI",
    "Yu Gothic UI",
    "Meiryo",
    sans-serif;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at center, #17243a 0%, #070b12 60%, #000000 100%);
  color: #f7f9ff;
}

button,
input {
  font: inherit;
}

button {
  -webkit-tap-highlight-color: transparent;
}

.timer-page {
  display: grid;
  min-height: 100vh;
  place-items: center;
  padding: 24px;
}

.timer-panel {
  width: min(1200px, 100%);
  padding: clamp(20px, 3vw, 42px);
  border: 1px solid rgba(182, 211, 255, 0.55);
  border-radius: clamp(24px, 4vw, 56px);
  background:
    linear-gradient(
      145deg,
      rgba(32, 50, 78, 0.92),
      rgba(6, 12, 23, 0.96)
    );
  box-shadow:
    0 0 30px rgba(116, 168, 255, 0.18),
    inset 0 0 28px rgba(158, 200, 255, 0.08);
  text-align: center;
}

.timer-title {
  max-width: 100%;
  margin: 0 auto clamp(8px, 1.6vw, 18px);
  padding: 4px 18px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: #eef5ff;
  font-size: clamp(2rem, 5vw, 4.8rem);
  font-weight: 700;
  line-height: 1.15;
  cursor: text;
  text-shadow: 0 0 16px rgba(159, 198, 255, 0.32);
  overflow-wrap: anywhere;
}

.timer-title:hover,
.timer-title:focus-visible {
  border-bottom-color: rgba(164, 202, 255, 0.7);
  outline: none;
}

.error-message {
  min-height: 1.5em;
  margin: 0;
  color: #ff9a9a;
  font-weight: 700;
}

.time-editor {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(4px, 1vw, 18px);
  width: 100%;
  margin: 0 auto clamp(20px, 4vw, 42px);
}

.time-unit {
  display: grid;
  min-width: 0;
  justify-items: center;
}

.time-number {
  width: 100%;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: #ffffff;
  font-family: Consolas, "Roboto Mono", "Courier New", monospace;
  font-size: clamp(6rem, 23vw, 20rem);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.08em;
  cursor: text;
  text-shadow:
    0 0 12px rgba(255, 255, 255, 0.5),
    0 0 28px rgba(153, 194, 255, 0.2);
}

.time-number:hover,
.time-number:focus-visible {
  outline: none;
  text-shadow:
    0 0 16px rgba(255, 255, 255, 0.72),
    0 0 36px rgba(125, 177, 255, 0.34);
}

.time-colon {
  align-self: center;
  padding-bottom: clamp(18px, 4vw, 52px);
  font-family: Consolas, "Roboto Mono", "Courier New", monospace;
  font-size: clamp(5rem, 13vw, 13rem);
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 0 16px rgba(255, 255, 255, 0.45);
}

.adjust-button {
  width: clamp(44px, 5vw, 64px);
  min-height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #91a8c8;
  font-size: clamp(1rem, 2vw, 1.8rem);
  cursor: pointer;
  opacity: 0.72;
  transition:
    color 0.15s ease,
    opacity 0.15s ease,
    transform 0.15s ease;
}

.adjust-button:hover,
.adjust-button:focus-visible {
  color: #d9e8ff;
  opacity: 1;
  outline: none;
  transform: scale(1.12);
}

.buttons {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(10px, 1.8vw, 22px);
}

.buttons button {
  min-height: 64px;
  padding: 14px 18px;
  border: 1px solid rgba(151, 184, 227, 0.58);
  border-radius: 18px;
  background: rgba(21, 34, 54, 0.9);
  color: #eaf2ff;
  font-size: clamp(1rem, 2vw, 1.8rem);
  font-weight: 700;
  cursor: pointer;
  box-shadow: inset 0 0 16px rgba(122, 169, 236, 0.06);
}

#start-button {
  border-color: #84b6ff;
  background: linear-gradient(145deg, #376cff, #1541b8);
  box-shadow:
    0 0 22px rgba(71, 126, 255, 0.45),
    inset 0 0 12px rgba(255, 255, 255, 0.12);
}

#reset-button {
  color: #dce8fa;
}

.buttons button:hover:not(:disabled),
.buttons button:focus-visible:not(:disabled) {
  filter: brightness(1.14);
  outline: 3px solid rgba(155, 198, 255, 0.32);
  outline-offset: 3px;
}

.buttons button:disabled,
.adjust-button:disabled,
.time-number:disabled,
.timer-title:disabled {
  cursor: not-allowed;
  opacity: 0.35;
}

.timer-panel.is-running .adjust-button {
  visibility: hidden;
}

@media (max-width: 720px) {
  .timer-page {
    padding: 12px;
  }

  .timer-panel {
    padding: 20px 14px;
    border-radius: 28px;
  }

  .time-editor {
    gap: 0;
  }

  .time-number {
    font-size: clamp(5rem, 28vw, 10rem);
  }

  .time-colon {
    padding-bottom: 28px;
    font-size: clamp(3.8rem, 13vw, 6rem);
  }

  .buttons {
    grid-template-columns: 1fr;
  }

  .buttons button {
    width: 100%;
    min-height: 56px;
  }
}

@media (max-height: 560px) and (orientation: landscape) {
  .timer-page {
    padding: 8px;
  }

  .timer-panel {
    padding: 12px 22px;
  }

  .timer-title {
    margin-bottom: 0;
    font-size: clamp(1.5rem, 4vw, 2.5rem);
  }

  .error-message {
    min-height: 1.2em;
    font-size: 0.85rem;
  }

  .time-editor {
    margin-bottom: 8px;
  }

  .time-number {
    font-size: clamp(4rem, 17vw, 8rem);
  }

  .time-colon {
    padding-bottom: 24px;
    font-size: clamp(3rem, 9vw, 5rem);
  }

  .adjust-button {
    min-height: 30px;
  }

  .buttons {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .buttons button {
    min-height: 48px;
    padding: 8px;
    font-size: 1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    scroll-behavior: auto !important;
    transition: none !important;
  }
}

入力後、Ctrl+Sを押して保存します。

画面幅に合わせて大きさを変える

今回のCSSでは、固定した文字サイズだけを使わず、clamp()を使って最小値、画面幅に応じた値、最大値を指定しています。

font-size: clamp(6rem, 23vw, 20rem);

画面が狭いときは小さく、広いときは大きくなります。ただし、小さくなりすぎたり、大きくなりすぎたりしないよう上下限も設けています。

メディアクエリで配置を切り替える

スマートフォンの縦画面では、3つのボタンを横へ並べると窮屈になります。画面幅が720px以下の場合は、ボタンを縦並びへ切り替えます。

@media (max-width: 720px) {
  .buttons {
    grid-template-columns: 1fr;
  }
}

横画面で高さが少ない場合は、余白や文字サイズを抑えて、画面内へ収まりやすくしています。

script.jsを書き換える

最後に、script.jsの内容をすべて次のコードへ置き換えます。

script.js のソースコードを表示
const panel = document.querySelector(".timer-panel");
const titleButton = document.getElementById("timer-title");
const minutesDisplay = document.getElementById("minutes-display");
const secondsDisplay = document.getElementById("seconds-display");
const adjustButtons = document.querySelectorAll(".adjust-button");
const errorMessage = document.getElementById("error-message");

const startButton = document.getElementById("start-button");
const pauseButton = document.getElementById("pause-button");
const resetButton = document.getElementById("reset-button");

let selectedMinutes = 5;
let selectedSeconds = 0;

let timerId = null;
let endTime = 0;
let remainingMilliseconds = 0;
let isRunning = false;
let isPaused = false;

function padNumber(value) {
  return String(value).padStart(2, "0");
}

function formatTime(milliseconds) {
  const totalSeconds = Math.max(0, Math.ceil(milliseconds / 1000));
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;

  return {
    minutes: padNumber(minutes),
    seconds: padNumber(seconds)
  };
}

function setError(message = "") {
  errorMessage.textContent = message;
}

function updateSelectedDisplay() {
  minutesDisplay.textContent = padNumber(selectedMinutes);
  secondsDisplay.textContent = padNumber(selectedSeconds);
}

function updateRunningDisplay(milliseconds) {
  const formatted = formatTime(milliseconds);
  minutesDisplay.textContent = formatted.minutes;
  secondsDisplay.textContent = formatted.seconds;
}

function setEditingEnabled(enabled) {
  titleButton.disabled = !enabled;
  minutesDisplay.disabled = !enabled;
  secondsDisplay.disabled = !enabled;

  adjustButtons.forEach((button) => {
    button.disabled = !enabled;
  });
}

function getSelectedMilliseconds() {
  if (selectedMinutes === 0 && selectedSeconds === 0) {
    setError("0分0秒では開始できません。");
    return null;
  }

  setError();
  return (selectedMinutes * 60 + selectedSeconds) * 1000;
}

function changeTime(target, change) {
  if (isRunning || isPaused) {
    return;
  }

  if (target === "minutes") {
    selectedMinutes = Math.max(0, selectedMinutes + change);
  }

  if (target === "seconds") {
    selectedSeconds = Math.min(59, Math.max(0, selectedSeconds + change));
  }

  updateSelectedDisplay();
  setError();
}

function askForNumber(label, currentValue, minimum, maximum = null) {
  const value = window.prompt(
    `${label}を入力してください。`,
    String(currentValue)
  );

  if (value === null) {
    return null;
  }

  const number = Number(value);

  if (
    !Number.isInteger(number) ||
    number < minimum ||
    (maximum !== null && number > maximum)
  ) {
    const range =
      maximum === null
        ? `${minimum}以上の整数`
        : `${minimum}~${maximum}の整数`;

    setError(`${label}は${range}で入力してください。`);
    return null;
  }

  setError();
  return number;
}

function editMinutes() {
  const value = askForNumber("分", selectedMinutes, 0);

  if (value === null) {
    return;
  }

  selectedMinutes = value;
  updateSelectedDisplay();
}

function editSeconds() {
  const value = askForNumber("秒", selectedSeconds, 0, 59);

  if (value === null) {
    return;
  }

  selectedSeconds = value;
  updateSelectedDisplay();
}

function editTitle() {
  const value = window.prompt(
    "タイマー名を入力してください。",
    titleButton.textContent.trim()
  );

  if (value === null) {
    return;
  }

  const normalizedTitle = value.trim();
  titleButton.textContent = normalizedTitle || "タイマー1";
}

function updateDisplay() {
  if (!isRunning) {
    return;
  }

  remainingMilliseconds = Math.max(0, endTime - Date.now());
  updateRunningDisplay(remainingMilliseconds);

  if (remainingMilliseconds <= 0) {
    finishTimer();
  }
}

function startTimer() {
  if (isRunning) {
    return;
  }

  if (!isPaused) {
    const selectedMilliseconds = getSelectedMilliseconds();

    if (selectedMilliseconds === null) {
      return;
    }

    remainingMilliseconds = selectedMilliseconds;
  }

  endTime = Date.now() + remainingMilliseconds;
  isRunning = true;
  isPaused = false;

  panel.classList.add("is-running");
  setEditingEnabled(false);

  startButton.textContent = "▶ スタート";
  startButton.disabled = true;
  pauseButton.disabled = false;

  updateDisplay();
  timerId = window.setInterval(updateDisplay, 200);
}

function pauseTimer() {
  if (!isRunning) {
    return;
  }

  remainingMilliseconds = Math.max(0, endTime - Date.now());

  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = true;

  panel.classList.remove("is-running");
  updateRunningDisplay(remainingMilliseconds);

  startButton.textContent = "▶ 再開";
  startButton.disabled = false;
  pauseButton.disabled = true;
}

function resetTimer() {
  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = false;
  endTime = 0;
  remainingMilliseconds = 0;

  panel.classList.remove("is-running");
  setEditingEnabled(true);

  selectedMinutes = 5;
  selectedSeconds = 0;
  updateSelectedDisplay();

  startButton.textContent = "▶ スタート";
  startButton.disabled = false;
  pauseButton.disabled = true;

  setError();
}

function finishTimer() {
  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = false;
  remainingMilliseconds = 0;

  panel.classList.remove("is-running");
  updateRunningDisplay(0);

  startButton.disabled = true;
  pauseButton.disabled = true;
  setEditingEnabled(false);
}

adjustButtons.forEach((button) => {
  button.addEventListener("click", () => {
    const target = button.dataset.target;
    const change = Number(button.dataset.change);
    changeTime(target, change);
  });
});

titleButton.addEventListener("click", editTitle);
minutesDisplay.addEventListener("click", editMinutes);
secondsDisplay.addEventListener("click", editSeconds);

startButton.addEventListener("click", startTimer);
pauseButton.addEventListener("click", pauseTimer);
resetButton.addEventListener("click", resetTimer);

updateSelectedDisplay();

入力後、Ctrl+Sを押して保存します。

タイマー名を押して変更する

上部の「タイマー1」を押すと、入力画面が表示されます。たとえば「課題3」と入力すると、画面上部の表示も「課題3」へ変わります。

何も入力せずに確定した場合は、デフォルトの「タイマー1」へ戻します。

スタート後は調整できないようにする

カウントダウン中に誤って時間が変わらないよう、スタート後はタイマー名、大きな数字、△▽を操作できなくします。△▽は非表示になり、残り時間だけが見やすく残ります。

今回もパソコン時刻との差で計算する

第1回と同じく、数字を1秒ずつ単純に減らすのではなく、終了予定時刻と現在のパソコン時刻との差から残り時間を求めています。

パソコンで動作を確認する

02フォルダ内のindex.htmlをWebブラウザで開きます。

  1. 画面上部に「タイマー1」が表示される
  2. 大きな時計が05:00と表示される
  3. 分と秒の△▽で数字を変更できる
  4. 大きな分・秒を押して直接入力できる
  5. 「タイマー1」を押して名前を変更できる
  6. スタート、一時停止、再開、リセットが動く
  7. スタート後に△▽が消える
  8. 0秒で停止する

秒は0~59の範囲で入力します。0分0秒ではスタートできません。

スマートフォン表示を確認する

実際のスマートフォンへ公開する前でも、Webブラウザの開発者ツールを使えば、画面幅を狭くして表示を確認できます。

まずは縦長の画面にして、時計、△▽、タイマー名、ボタンが画面からはみ出していないか確認します。

続いて横画面も確認します。横画面では横幅は広くなりますが、高さが少なくなるため、余白と時計の大きさを自動的に抑えています。

Xserverへ第2回の完成版を公開する

ローカルで動作確認できたら、Xserverのファイルマネージャーへアップロードします。

第1回の完成版を残したまま、次のフォルダへ第2回の3ファイルを配置します。

/public_html/tools/kitchen-timer/02/
├─ index.html
├─ style.css
└─ script.js

公開後は、次のURLをWebブラウザで開いて動作を確認します。

https://nonbiri-lab.jp/tools/kitchen-timer/02/

今回の完成版

第2回の完成版を別画面で開く

まとめ

  • 複数の画像案から、青いガラス風の標準デザインを選んだ
  • 下の入力欄をなくし、大きな時計へ△▽を付けた
  • 大きな数字を押して、分と秒を直接入力できるようにした
  • 上部の「タイマー1」を押して名前を変更できるようにした
  • スタート後は調整操作を隠して、残り時間を見やすくした
  • viewport、可変文字サイズ、メディアクエリでスマートフォンへ対応した
  • 01を残したまま、02として新しい完成版を公開した

これで、単体タイマーとしての見た目と操作方法がかなり使いやすくなりました。

次回は、タイマー終了時の見せ方を追加します。0秒になったパネルを回転させ、「タイマー名+終了」と大きく表示する予定です。