公開日: 2025-07-28著者: Yov in YovStudio
今回やること
前回、ChatGPT が作ったコードに手を加えたことでアプリとしての基本機能はひと通り形になってきました。
でも、それで「使いやすい」と言えるでしょうか?
今のところ、以下のような使い勝手の課題があります。
- スマホなど横幅の狭い画面で横スクロールをすると、カレンダーだけでなく画面全体がスクロールしてしまう
- テーマカラーやライト/ダークモードの選択が文字になっており、直感的でない
- 表示する年が増えると気分入力エリアがどんどん下に押し出され、入力するにはスクロールが必要になる
- 気分アイコンと、カレンダーに表示される色との関係が視覚的にわかりにくい
今回は、これらの課題を解消するため、UIのブラッシュアップに取り組みます。 小さな変更でも、全体の印象や使いやすさに大きな変化を与えることがあります。
今回使うプロンプト
今回やることをベースにプロンプトを作成しました。
前々回のチャットスレッドをそのまま使って、継続して送ってみます。
作成済みの「MOOD TRACKER」のWebアプリを改善します。
以下の目的と前提条件に基づき、変更内容をすべて反映してください。
## 目的
ユーザーの視点に立って、使いやすさを改善する
## 前提条件
- コードは HTML / CSS / JavaScript で構成された前回のアプリの続きとして書いてください。
- React や外部ライブラリは使わず、ブラウザ上でローカル実行できるようにしてください。
- コードは省略せず、すべてフルで提供してください。
## 変更内容
1. テーマカラー選択UIの改善
- 現在、"Green" や "Blue" などの文字列を選ぶ形になっていますが、カラーパレットのようなUIに変えてください。
- 色の見本(丸いボタンや四角いボタン)を視覚的に配置し、選択状態がわかるようにしてください。
- 使用する色は現在アプリで使用されている5色をそのまま用いてください。
2. ダークモード/ライトモードの切り替えUIの改善
- 現在、"LightMode" や "DarkMode" など文字列で表示していますが、太陽と月のアイコンボタンで切り替えるようにしてください。
3. 気分の入力エリアの表示位置の改善
- カレンダーの年が増えてくると、気分の入力エリアの表示にスクロールが必要になってしまいます。気分の入力エリアは画面の最下部に固定し、カレンダーの数に影響されないようにしてください。
4. 気分アイコンと気分の色の紐付けを視覚的に伝わるように改善
- 気分アイコンそれぞれに、気分の色をつけてください。
5. 横スクロールの改善
- 現在、カレンダーの横幅が画面幅に収まらない場合、横スクロールすると画面全体が横スクロールしてしまっています。カレンダーだけを横スクロール可能にしてください。
- 横スクロールは年ごとのカレンダーそれぞれ個別にできるものとし、曜日ラベルはスクロールさせずに固定してください。
ChatGPT からの返答
今回も約60秒で、コードと改善内容のサマリーが返ってきました。
その内容は以下のとおりです。
※補足:同じプロンプトでも、ChatGPT は状況によって違った回答を返すことがあります。自分で試してみたら全然違う結果になった、というのもよくあることです。その違いも含めて楽しんでもらえればと思います。
コード
コードは長いので折りたたんでいます。
コード内に含まれるコメントも含め、すべて ChatGPT が作成したものです。
コードはこちら
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MOOD TRACKER</title>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<style>
:root {
--bg-light: #f0f0f0;
--bg-dark: #1e1e2f;
--text-light: #333;
--text-dark: #eee;
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.15);
--neutral-light: rgba(0, 0, 0, 0.06);
--neutral-dark: rgba(255, 255, 255, 0.06);
/* カラーパレット */
--green1: #b0e8c3;
--green2: #98d1b6;
--green3: #63bc92;
--green4: #33a66e;
--green5: #198f56;
--blue1: #d1f2f9;
--blue2: #9edbf0;
--blue3: #68c6e6;
--blue4: #30b0dc;
--blue5: #008ac1;
--red1: #ffd6d6;
--red2: #ff9c9c;
--red3: #ff6c6c;
--red4: #ff3c3c;
--red5: #d10000;
--purple1: #e6d9f0;
--purple2: #d1aee4;
--purple3: #b77dd7;
--purple4: #9f49cb;
--purple5: #7d2ea6;
--orange1: #ffeacc;
--orange2: #ffd099;
--orange3: #ffb566;
--orange4: #ff9a33;
--orange5: #cc6f00;
}
body {
margin: 0;
font-family: sans-serif;
background: var(--bg-light);
color: var(--text-light);
transition:
background 0.3s,
color 0.3s;
padding-bottom: 100px; /* 気分入力エリア分のスペース確保 */
}
body.dark {
background: var(--bg-dark);
color: var(--text-dark);
}
h1 {
font-size: 16px;
}
.glass {
backdrop-filter: blur(12px);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
padding: 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
}
/* テーマカラー選択パレット */
.theme-palette {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 10px;
}
.color-button {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2.5px solid transparent;
cursor: pointer;
transition: border-color 0.3s;
box-sizing: border-box;
}
.color-button.green {
background-color: var(--green5);
}
.color-button.blue {
background-color: var(--blue5);
}
.color-button.red {
background-color: var(--red5);
}
.color-button.purple {
background-color: var(--purple5);
}
.color-button.orange {
background-color: var(--orange5);
}
.color-button.selected {
border-color: #000;
}
body.dark .color-button.selected {
border-color: #fff;
}
/* ダークモード切替ボタン */
.mode-toggle {
cursor: pointer;
width: 36px;
height: 36px;
border: none;
background: none;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.mode-toggle svg {
width: 24px;
height: 24px;
fill: currentColor;
transition: opacity 0.3s;
color: var(--text-light);
}
body.dark .mode-toggle svg {
color: var(--text-dark);
}
.mode-toggle .sun {
opacity: 1;
}
.mode-toggle.dark .sun {
opacity: 0;
}
.mode-toggle .moon {
opacity: 0;
}
.mode-toggle.dark .moon {
opacity: 1;
}
/* カレンダー */
.calendar {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 1rem;
}
.year-section {
display: flex;
flex-direction: column;
gap: 3px;
}
.year-section h2 {
font-size: 14px;
margin: 0 0 0.3rem 0;
}
.calendar-wrapper {
display: flex;
flex-direction: row;
}
.calendar-scrollable {
display: flex;
flex-direction: column;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding-left: 3px;
}
.calendar-scrollable::-webkit-scrollbar {
height: 8px;
}
.calendar-scrollable::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
border-radius: 4px;
}
.month-labels {
overflow: visible;
display: flex;
gap: 3px;
font-size: 0.65rem;
word-break: keep-all;
user-select: none;
}
.month-labels span {
min-width: 16px;
max-width: 16px;
text-align: center;
}
.calendar-body {
display: flex;
max-width: 100%;
}
/* 曜日ラベル縦列固定 */
.day-labels {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 0.65rem;
line-height: 16px;
min-width: 30px;
max-width: 30px;
user-select: none;
position: sticky;
left: 0;
z-index: 10;
padding-top: 16px;
}
body.dark .day-labels {
background: var(--bg-dark);
}
/* 日付列 */
.day-col {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 19px;
user-select: none;
}
.day {
width: 16px;
height: 16px;
border-radius: 3px;
cursor: pointer;
background: var(--neutral-light);
box-sizing: border-box;
transition:
transform 0.1s,
border 0.2s;
border: 1px solid transparent;
}
body.dark .day {
background: var(--neutral-dark);
}
.day:hover {
transform: scale(1.3);
}
.day.selected {
border: 2px solid #000;
}
body.dark .day.selected {
border: 2px solid #fff;
}
/* 気分入力エリアを画面下部に固定 */
.legend {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--glass-bg);
border-top: 1px solid var(--glass-border);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(12px);
padding: 0.8rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
z-index: 20;
user-select: none;
}
#selectedDateDisplay {
font-size: 1rem;
font-weight: bold;
}
.mood-row {
display: flex;
gap: 1rem;
}
.mood-icon {
font-size: 1.8rem;
cursor: pointer;
opacity: 1;
border-radius: 8px;
padding: 4px;
border: 1px solid #aaa;
transition: border-color 0.3s;
user-select: none;
}
.mood-icon.active {
box-shadow: 0 0 1px 1px currentColor;
}
</style>
</head>
<body>
<header class="glass">
<h1>MOOD TRACKER</h1>
<div class="controls" style="display: flex; align-items: center; gap: 1rem">
<div class="theme-palette" aria-label="テーマカラー選択" role="radiogroup" tabindex="0">
<button class="color-button green" aria-label="Green" data-theme="green" role="radio" tabindex="0"></button>
<button class="color-button blue" aria-label="Blue" data-theme="blue" role="radio" tabindex="-1"></button>
<button class="color-button red" aria-label="Red" data-theme="red" role="radio" tabindex="-1"></button>
<button class="color-button purple" aria-label="Purple" data-theme="purple" role="radio" tabindex="-1"></button>
<button class="color-button orange" aria-label="Orange" data-theme="orange" role="radio" tabindex="-1"></button>
</div>
<button class="mode-toggle" aria-label="ダークモード切替" title="ダークモード切替" type="button">
<!-- 太陽アイコン -->
<svg class="sun" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="5" />
<g stroke="currentColor" stroke-width="2">
<line x1="12" y1="1" x2="12" y2="4" />
<line x1="12" y1="20" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34" />
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="4" y2="12" />
<line x1="20" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66" />
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22" />
</g>
</svg>
<!-- 月アイコン -->
<svg class="moon" viewBox="0 0 24 24" aria-hidden="true" focusable="false" style="position: absolute; top: 6px; left: 6px">
<path d="M21 12.79A9 9 0 0111.21 3a7 7 0 000 14 9 9 0 009.79-4.21z" />
</svg>
</button>
</div>
</header>
<main>
<div class="calendar" id="calendar"></div>
</main>
<div class="legend glass" id="legend">
<div id="selectedDateDisplay">日付を選択してください</div>
<div class="mood-row" role="radiogroup" aria-label="気分選択">
<span class="mood-icon" data-mood="1" role="radio" tabindex="0" aria-checked="false" aria-label="悲しい">😞</span>
<span class="mood-icon" data-mood="2" role="radio" tabindex="-1" aria-checked="false" aria-label="普通">😐</span>
<span class="mood-icon" data-mood="3" role="radio" tabindex="-1" aria-checked="false" aria-label="まあまあ">🙂</span>
<span class="mood-icon" data-mood="4" role="radio" tabindex="-1" aria-checked="false" aria-label="嬉しい">😊</span>
<span class="mood-icon" data-mood="5" role="radio" tabindex="-1" aria-checked="false" aria-label="とても嬉しい">😁</span>
</div>
</div>
<script>
const calendarEl = document.getElementById('calendar');
const themePalette = document.querySelectorAll('.color-button');
const modeToggleBtn = document.querySelector('.mode-toggle');
const moodIcons = document.querySelectorAll('.mood-icon');
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
let currentTheme = localStorage.getItem('theme') || 'green';
let selectedDate = new Date().toISOString().split('T')[0];
let isDarkMode = localStorage.getItem('mode') === 'dark';
if (isDarkMode) document.body.classList.add('dark');
updateModeToggleUI();
// テーマカラー選択ボタンの初期選択状態をセット
function updateThemeSelectionUI() {
themePalette.forEach((btn) => {
const theme = btn.dataset.theme;
if (theme === currentTheme) {
btn.classList.add('selected');
btn.setAttribute('aria-checked', 'true');
btn.setAttribute('tabindex', '0');
} else {
btn.classList.remove('selected');
btn.setAttribute('aria-checked', 'false');
btn.setAttribute('tabindex', '-1');
}
});
}
updateThemeSelectionUI();
// ダークモード切替ボタンのUI反映
function updateModeToggleUI() {
if (document.body.classList.contains('dark')) {
modeToggleBtn.classList.add('dark');
modeToggleBtn.setAttribute('aria-pressed', 'true');
modeToggleBtn.title = 'ライトモードに切り替え';
} else {
modeToggleBtn.classList.remove('dark');
modeToggleBtn.setAttribute('aria-pressed', 'false');
modeToggleBtn.title = 'ダークモードに切り替え';
}
}
// カラーパレットクリック時
themePalette.forEach((btn) => {
btn.addEventListener('click', () => {
currentTheme = btn.dataset.theme;
localStorage.setItem('theme', currentTheme);
updateThemeSelectionUI();
buildCalendar();
});
btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
btn.click();
}
});
});
// ダークモード切替
modeToggleBtn.addEventListener('click', () => {
document.body.classList.toggle('dark');
isDarkMode = document.body.classList.contains('dark');
localStorage.setItem('mode', isDarkMode ? 'dark' : 'light');
updateModeToggleUI();
buildCalendar();
});
// 気分データの取得・保存
let moodData = JSON.parse(localStorage.getItem('moodData') || '{}');
// 気分の色パレット
function getColorScale(mood, theme, isDark) {
const palette = {
light: {
green: ['#b0e8c3', '#98d1b6', '#63bc92', '#33a66e', '#198f56'],
blue: ['#d1f2f9', '#9edbf0', '#68c6e6', '#30b0dc', '#008ac1'],
red: ['#ffd6d6', '#ff9c9c', '#ff6c6c', '#ff3c3c', '#d10000'],
purple: ['#e6d9f0', '#d1aee4', '#b77dd7', '#9f49cb', '#7d2ea6'],
orange: ['#ffeacc', '#ffd099', '#ffb566', '#ff9a33', '#cc6f00'],
},
dark: {
green: ['#243a2c', '#246647', '#2ca37a', '#34dca0', '#6fffc8'],
blue: ['#22435e', '#24729c', '#269bd5', '#28c1fc', '#68f0ff'],
red: ['#3d1e1e', '#842323', '#d33434', '#f95858', '#ffaaaa'],
purple: ['#38234f', '#562b63', '#813a94', '#a856c0', '#ce9ef0'],
orange: ['#3f2c1a', '#7c4c00', '#b36a00', '#ff8c1a', '#ffc180'],
},
};
return palette[isDark ? 'dark' : 'light'][theme][mood - 1];
}
function formatDateKey(date) {
return date.toISOString().split('T')[0];
}
// カレンダー生成
let isFirstBuild = true;
function buildCalendar() {
let allDates = Object.keys(moodData);
let years = [...new Set(allDates.map((d) => d.split('-')[0]))];
const thisYear = new Date().getFullYear().toString();
if (!years.includes(thisYear)) years.push(thisYear);
years = years.sort((a, b) => b - a);
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
if (isFirstBuild) {
calendarEl.innerHTML = ''; // 初回のみ全描画
years.forEach((year) => {
const section = document.createElement('div');
section.className = 'year-section';
section.dataset.year = year;
const title = document.createElement('h2');
title.textContent = year;
section.appendChild(title);
const calendarWrapper = document.createElement('div');
calendarWrapper.className = 'calendar-wrapper';
const labelDays = document.createElement('div');
labelDays.className = 'day-labels';
dayLabels.forEach((d) => {
const label = document.createElement('div');
label.textContent = d;
labelDays.appendChild(label);
});
calendarWrapper.appendChild(labelDays);
const calendarScrollable = document.createElement('div');
calendarScrollable.className = 'calendar-scrollable';
const monthLabels = document.createElement('div');
monthLabels.className = 'month-labels';
monthLabels.dataset.year = year;
const calendarBody = document.createElement('div');
calendarBody.className = 'calendar-body';
calendarBody.dataset.year = year;
generateCalendarContent(year, monthLabels, calendarBody);
calendarScrollable.appendChild(monthLabels);
calendarScrollable.appendChild(calendarBody);
calendarWrapper.appendChild(calendarScrollable);
section.appendChild(calendarWrapper);
calendarEl.appendChild(section);
});
isFirstBuild = false;
} else {
years.forEach((year) => {
const calendarBody = calendarEl.querySelector(`.calendar-body[data-year="${year}"]`);
const monthLabels = calendarEl.querySelector(`.month-labels[data-year="${year}"]`);
if (!calendarBody || !monthLabels) return;
calendarBody.innerHTML = '';
monthLabels.innerHTML = '';
generateCalendarContent(year, monthLabels, calendarBody);
});
}
selectedDateDisplay.textContent = selectedDate;
updateMoodSelectionUI();
}
function generateCalendarContent(year, monthLabels, calendarBody) {
const startDate = new Date(`${year}-01-01`);
const endDate = new Date(`${year}-12-31`);
const firstSunday = new Date(startDate);
while (firstSunday.getDay() !== 0) firstSunday.setDate(firstSunday.getDate() - 1);
let lastMonth = -1;
for (let d = new Date(firstSunday); d <= endDate; d.setDate(d.getDate() + 7)) {
const col = document.createElement('div');
col.className = 'day-col';
const cellMonth = d.getMonth();
const isFirstLoop = lastMonth === -1;
const isMonthChanged = lastMonth !== cellMonth;
const isJanuary = cellMonth === 0;
if (isMonthChanged && (!isFirstLoop || isJanuary)) {
const label = document.createElement('span');
label.textContent = `${cellMonth + 1}月`;
monthLabels.appendChild(label);
} else {
const blank = document.createElement('span');
blank.textContent = '';
monthLabels.appendChild(blank);
}
lastMonth = cellMonth;
for (let i = 0; i < 7; i++) {
const cellDate = new Date(d);
cellDate.setDate(cellDate.getDate() + i);
const key = formatDateKey(cellDate);
const day = document.createElement('div');
day.className = 'day';
const mood = moodData[key];
const isDark = document.body.classList.contains('dark');
if (mood) {
day.style.background = getColorScale(mood, currentTheme, isDark);
}
if (selectedDate === key) {
day.classList.add('selected');
}
day.title = key;
day.onclick = () => openMoodSelector(key);
col.appendChild(day);
}
calendarBody.appendChild(col);
}
}
// 気分入力エリアのUI更新
function updateMoodSelectionUI() {
moodIcons.forEach((icon) => {
icon.style.background = getColorScale(parseInt(icon.dataset.mood), currentTheme, isDarkMode);
if (!selectedDate) {
icon.classList.remove('active');
icon.setAttribute('aria-checked', 'false');
icon.setAttribute('tabindex', '-1');
return;
}
const mood = moodData[selectedDate];
if (parseInt(icon.dataset.mood) === mood) {
icon.classList.add('active');
icon.setAttribute('aria-checked', 'true');
icon.setAttribute('tabindex', '0');
} else {
icon.classList.remove('active');
icon.setAttribute('aria-checked', 'false');
icon.setAttribute('tabindex', '-1');
}
});
}
// 日付選択&気分入力開始
function openMoodSelector(dateKey) {
selectedDate = dateKey;
selectedDateDisplay.textContent = dateKey;
updateMoodSelectionUI();
buildCalendar();
}
// 気分アイコン選択
moodIcons.forEach((icon) => {
icon.onclick = () => {
if (!selectedDate) return;
const mood = parseInt(icon.dataset.mood);
moodData[selectedDate] = mood;
localStorage.setItem('moodData', JSON.stringify(moodData));
updateMoodSelectionUI();
buildCalendar();
};
icon.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
icon.click();
}
});
});
buildCalendar();
</script>
</body>
</html>
チャットの回答
改善内容をすべて反映しました。今回の更新で以下の点が実現されています:
テーマカラー選択UI
→ カラーパレットを丸い色ボタンで実装。現在のテーマはボーダーで強調表示。
ダークモード/ライトモード切替
→ 文字ボタンをやめて、太陽🌞と月🌙のアイコンボタンで切替。状態に応じてアイコンの透過変化。
気分入力エリア固定
→ .legendを画面下部に固定し、常に見えるように。カレンダーは画面上部に。
気分アイコンに気分色の背景色を付ける
→ アイコンの周囲に気分色を付けて視認性アップ。
横スクロール改善
→ 年ごとのカレンダーに横スクロールを個別に付与。
→ 曜日ラベルはスクロールせず左端に固定。
もし動作や見た目で気になるところや追加要望があれば教えてください!
この修正でユーザー視点での使いやすさがかなり向上しているはずです。
見た目が整った
さて、どんなふうに仕上がったでしょうか?
さっそくチェックしてみましょう。
とりあえず動きを見たい方は下記リンクからどうぞ。
動作確認用デモページ
実際に手元で動かしてみたい方は、下記からダウンロード後、zip ファイルを展開して中に入っている index.html を実行してみてください。
zipファイルをダウンロード
── おぉ。かなり見た目も整ったんじゃないでしょうか。カレンダーの横スクロールが整い、スマホのような横幅が狭い表示でもレイアウトが崩れなくなりました。また、テーマカラーや気分の色も直感的に伝わるようになりましたね。
ライトモード(紫):
ダークモード(緑):
AI と一緒につくる、ということ
ここまで全 7 章に渡り ChatGPT と一緒に Web アプリを作ってきて、紆余曲折ありましたが、なんとか当初描いていたイメージに近いものを作ることができました。
細かい改善ポイントはまだありますが、ひと通り仕上がったのでこのシリーズとしてはひと区切りしたいと思います。
当初のイメージ:
いかがでしたか?
AI は非常に優秀ですが、伝え方がとても大切であることが実感できましたね。
第 2 章では前提条件をしっかりと伝える大切さを学び、第 3 章ではそれっぽいものは作られたものの、人に向けた優しさを意識してプロンプトを作らないととても使えないものが出来上がってしまうことが分かりました。
第 4 章と第 5 章では AI に調整をしてもらいましたが、かゆいところに手が届かず、第 6 章では実際に自分でコードを手直しすることで、初めて「納得のいく動作」が実現できました。
ここでようやく、人と AI の共同作業のバランスが見えてきたように思います。
単に任せきりにするのではなく、役割分担の意識や、AI が書いたコードを読み取る力も必要なのだと再認識しました。
さいごに
ここまで読んでくださり、ありがとうございました。
正直、4 章や 5 章あたりで ChatGPT に修正を依頼しているときはなかなか前進せず、「大丈夫かな、このシリーズ」と思っていましたが、なんとか完結できてよかったです。
自分の主観は AI には分かりませんので、そのあたりをしっかり伝えるように意識しないといけません。
これは 対 AI だけでなく、人と関わっていく中でも大切ですよね。
いかに自分の言葉が普段から足りていないのかを痛感した次第です。
ただ、なんだかんだで思い描いたものに近いものが出来上がったのは楽しめました。
今後も AI とともに、こうした創作の楽しさを探っていけたらと思っています。