プレイリスト
6曲分の曲名・アーティスト・再生時間を一覧表示するプレイリスト。再生中の曲は番号がバーアニメーションに切り替わり、総再生時間もヘッダーに表示します。
ライブデモ
コード
HTML
<!-- プレイリスト: 曲リスト+再生中ハイライト(バーアニメ)+各曲の長さ+総再生時間 -->
<div class="stage">
<section class="mpl-playlist" aria-label="プレイリスト">
<header class="mpl-playlist__head">
<h2 class="mpl-playlist__title">夜想プレイリスト</h2>
<p class="mpl-playlist__count">6曲 ・ 21分17秒</p>
</header>
<ul class="mpl-playlist__list">
<li class="mpl-playlist__item is-active" data-duration="204">
<button type="button" class="mpl-playlist__row" aria-current="true">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">1</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">朝焼けのプレリュード</span>
<span class="mpl-playlist__artist">Sora Tone</span>
</span>
<span class="mpl-playlist__duration">3:24</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="242">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">2</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">硝子の街</span>
<span class="mpl-playlist__artist">水城かおり</span>
</span>
<span class="mpl-playlist__duration">4:02</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="171">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">3</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">潮風とページ</span>
<span class="mpl-playlist__artist">Blue Note Diner</span>
</span>
<span class="mpl-playlist__duration">2:51</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="217">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">4</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">午後三時のワルツ</span>
<span class="mpl-playlist__artist">ミナモ</span>
</span>
<span class="mpl-playlist__duration">3:37</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="255">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">5</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">遠回りの帰り道</span>
<span class="mpl-playlist__artist">Kenta Ogawa</span>
</span>
<span class="mpl-playlist__duration">4:15</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="188">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">6</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">灯りをつけて</span>
<span class="mpl-playlist__artist">yura</span>
</span>
<span class="mpl-playlist__duration">3:08</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
</ul>
</section>
</div>
CSS
/* プレイリスト: 紙のように上品なオフホワイト+ミントのアクセント */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
padding: 24px;
display: grid;
place-items: center;
font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 10% 8%, #e3f3ec 0%, transparent 45%),
radial-gradient(circle at 92% 90%, #eef1e0 0%, transparent 48%),
#fbfaf7;
color: #23282a;
}
.stage { display: grid; place-items: center; width: 100%; }
.mpl-playlist {
width: min(420px, 92vw);
padding: 22px 8px 14px;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 20px 48px rgba(30, 40, 36, .1);
}
.mpl-playlist__head { padding: 0 18px 14px; border-bottom: 1px solid #eef0ec; margin-bottom: 6px; }
.mpl-playlist__title { margin: 0 0 4px; font-size: 16.5px; font-weight: 800; letter-spacing: .01em; }
.mpl-playlist__count { margin: 0; font-size: 12px; color: #8b958e; }
.mpl-playlist__list { list-style: none; margin: 0; padding: 6px; display: grid; gap: 2px; }
.mpl-playlist__item { position: relative; border-radius: 12px; overflow: hidden; }
.mpl-playlist__row {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: none;
background: transparent;
font: inherit;
text-align: left;
color: inherit;
cursor: pointer;
border-radius: 12px;
transition: background .16s ease;
}
.mpl-playlist__row:hover { background: #f3f7f2; }
.mpl-playlist__item.is-active .mpl-playlist__row { background: #e8f5ee; }
.mpl-playlist__index {
position: relative;
flex: none;
width: 26px;
height: 26px;
display: grid;
place-items: center;
}
.mpl-playlist__num { font-size: 12.5px; font-variant-numeric: tabular-nums; color: #9aa39c; }
.mpl-playlist__item.is-active .mpl-playlist__num { display: none; }
.mpl-playlist__eq {
display: none;
align-items: flex-end;
gap: 3px;
height: 15px;
}
.mpl-playlist__item.is-active .mpl-playlist__eq { display: flex; }
.mpl-playlist__eq i {
display: block;
width: 3px;
border-radius: 2px;
background: #2f9e6b;
transform: scaleY(.35);
animation: mpl-bounce 1s ease-in-out infinite;
animation-play-state: paused;
}
.mpl-playlist__item.is-active.is-playing .mpl-playlist__eq i { animation-play-state: running; }
.mpl-playlist__eq i:nth-child(1) { height: 8px; animation-delay: -.5s; }
.mpl-playlist__eq i:nth-child(2) { height: 14px; animation-delay: -.15s; }
.mpl-playlist__eq i:nth-child(3) { height: 10px; animation-delay: -.8s; }
@keyframes mpl-bounce { 0%, 100% { transform: scaleY(.35); } 50% { transform: scaleY(1); } }
.mpl-playlist__meta { min-width: 0; flex: 1; display: grid; }
.mpl-playlist__name {
font-size: 13.5px;
font-weight: 700;
color: #23282a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mpl-playlist__item.is-active .mpl-playlist__name { color: #1f7a4f; }
.mpl-playlist__artist { font-size: 11.5px; color: #8b958e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mpl-playlist__duration { flex: none; font-size: 11.5px; font-variant-numeric: tabular-nums; color: #9aa39c; }
.mpl-playlist__progress {
position: absolute;
left: 12px;
right: 12px;
bottom: 3px;
height: 2px;
border-radius: 2px;
background: rgba(47, 158, 107, .12);
display: none;
}
.mpl-playlist__item.is-active .mpl-playlist__progress { display: block; }
.mpl-playlist__progress-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 2px;
background: linear-gradient(90deg, #6cc79b, #2f9e6b);
}
@media (prefers-reduced-motion: reduce) {
.mpl-playlist__row { transition: none; }
.mpl-playlist__eq i { animation: none; }
}
JavaScript
// プレイリスト: 実音声は使わず、選択中の曲の進捗バーをタイマーで擬似的に進める
(() => {
const list = document.querySelector('.mpl-playlist__list');
if (!list) return; // null安全
const items = Array.from(list.querySelectorAll('.mpl-playlist__item'));
if (!items.length) return;
const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;
const TICK_MS = 200;
const SPEED = 5.5; // デモとして体感しやすい速さで擬似進行させる倍率
let activeIndex = items.findIndex((li) => li.classList.contains('is-active'));
if (activeIndex < 0) activeIndex = 0;
let current = 0;
let playing = false;
let timer = null;
function durationOf(li) { return parseFloat(li.dataset.duration) || 0; }
function fillOf(li) { return li.querySelector('.mpl-playlist__progress-fill'); }
function rowOf(li) { return li.querySelector('.mpl-playlist__row'); }
function renderActiveState() {
items.forEach((li, i) => {
const active = i === activeIndex;
li.classList.toggle('is-active', active);
li.classList.toggle('is-playing', active && playing);
const row = rowOf(li);
if (row) row.setAttribute('aria-current', String(active));
if (!active) {
const fill = fillOf(li);
if (fill) fill.style.width = '0%';
}
});
}
function renderProgress() {
const li = items[activeIndex];
const d = durationOf(li);
const fill = fillOf(li);
if (fill) fill.style.width = `${d ? Math.min(100, (current / d) * 100) : 0}%`;
}
function setPlaying(next) {
playing = next;
items[activeIndex].classList.toggle('is-playing', next);
clearInterval(timer);
if (next) {
timer = setInterval(() => {
current += (TICK_MS / 1000) * SPEED;
const d = durationOf(items[activeIndex]);
if (current >= d) { goTo(activeIndex + 1, true); return; }
renderProgress();
}, TICK_MS);
}
}
function goTo(index, keepPlaying) {
const len = items.length;
activeIndex = ((index % len) + len) % len;
current = 0;
renderActiveState();
renderProgress();
setPlaying(keepPlaying !== false);
}
items.forEach((li, i) => {
const row = rowOf(li);
if (!row) return;
row.addEventListener('click', () => {
if (i === activeIndex) { setPlaying(!playing); }
else { goTo(i, true); }
});
});
renderActiveState();
renderProgress();
// カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
if (!reduceMotion) setPlaying(true);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「プレイリスト」の効果を追加してください。
# 追加してほしい効果
プレイリスト(チャット & メディア)
6曲分の曲名・アーティスト・再生時間を一覧表示するプレイリスト。再生中の曲は番号がバーアニメーションに切り替わり、総再生時間もヘッダーに表示します。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- プレイリスト: 曲リスト+再生中ハイライト(バーアニメ)+各曲の長さ+総再生時間 -->
<div class="stage">
<section class="mpl-playlist" aria-label="プレイリスト">
<header class="mpl-playlist__head">
<h2 class="mpl-playlist__title">夜想プレイリスト</h2>
<p class="mpl-playlist__count">6曲 ・ 21分17秒</p>
</header>
<ul class="mpl-playlist__list">
<li class="mpl-playlist__item is-active" data-duration="204">
<button type="button" class="mpl-playlist__row" aria-current="true">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">1</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">朝焼けのプレリュード</span>
<span class="mpl-playlist__artist">Sora Tone</span>
</span>
<span class="mpl-playlist__duration">3:24</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="242">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">2</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">硝子の街</span>
<span class="mpl-playlist__artist">水城かおり</span>
</span>
<span class="mpl-playlist__duration">4:02</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="171">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">3</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">潮風とページ</span>
<span class="mpl-playlist__artist">Blue Note Diner</span>
</span>
<span class="mpl-playlist__duration">2:51</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="217">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">4</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">午後三時のワルツ</span>
<span class="mpl-playlist__artist">ミナモ</span>
</span>
<span class="mpl-playlist__duration">3:37</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="255">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">5</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">遠回りの帰り道</span>
<span class="mpl-playlist__artist">Kenta Ogawa</span>
</span>
<span class="mpl-playlist__duration">4:15</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
<li class="mpl-playlist__item" data-duration="188">
<button type="button" class="mpl-playlist__row" aria-current="false">
<span class="mpl-playlist__index">
<span class="mpl-playlist__num">6</span>
<span class="mpl-playlist__eq" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span class="mpl-playlist__meta">
<span class="mpl-playlist__name">灯りをつけて</span>
<span class="mpl-playlist__artist">yura</span>
</span>
<span class="mpl-playlist__duration">3:08</span>
</button>
<span class="mpl-playlist__progress" aria-hidden="true"><span class="mpl-playlist__progress-fill"></span></span>
</li>
</ul>
</section>
</div>
【CSS】
/* プレイリスト: 紙のように上品なオフホワイト+ミントのアクセント */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
padding: 24px;
display: grid;
place-items: center;
font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 10% 8%, #e3f3ec 0%, transparent 45%),
radial-gradient(circle at 92% 90%, #eef1e0 0%, transparent 48%),
#fbfaf7;
color: #23282a;
}
.stage { display: grid; place-items: center; width: 100%; }
.mpl-playlist {
width: min(420px, 92vw);
padding: 22px 8px 14px;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 20px 48px rgba(30, 40, 36, .1);
}
.mpl-playlist__head { padding: 0 18px 14px; border-bottom: 1px solid #eef0ec; margin-bottom: 6px; }
.mpl-playlist__title { margin: 0 0 4px; font-size: 16.5px; font-weight: 800; letter-spacing: .01em; }
.mpl-playlist__count { margin: 0; font-size: 12px; color: #8b958e; }
.mpl-playlist__list { list-style: none; margin: 0; padding: 6px; display: grid; gap: 2px; }
.mpl-playlist__item { position: relative; border-radius: 12px; overflow: hidden; }
.mpl-playlist__row {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: none;
background: transparent;
font: inherit;
text-align: left;
color: inherit;
cursor: pointer;
border-radius: 12px;
transition: background .16s ease;
}
.mpl-playlist__row:hover { background: #f3f7f2; }
.mpl-playlist__item.is-active .mpl-playlist__row { background: #e8f5ee; }
.mpl-playlist__index {
position: relative;
flex: none;
width: 26px;
height: 26px;
display: grid;
place-items: center;
}
.mpl-playlist__num { font-size: 12.5px; font-variant-numeric: tabular-nums; color: #9aa39c; }
.mpl-playlist__item.is-active .mpl-playlist__num { display: none; }
.mpl-playlist__eq {
display: none;
align-items: flex-end;
gap: 3px;
height: 15px;
}
.mpl-playlist__item.is-active .mpl-playlist__eq { display: flex; }
.mpl-playlist__eq i {
display: block;
width: 3px;
border-radius: 2px;
background: #2f9e6b;
transform: scaleY(.35);
animation: mpl-bounce 1s ease-in-out infinite;
animation-play-state: paused;
}
.mpl-playlist__item.is-active.is-playing .mpl-playlist__eq i { animation-play-state: running; }
.mpl-playlist__eq i:nth-child(1) { height: 8px; animation-delay: -.5s; }
.mpl-playlist__eq i:nth-child(2) { height: 14px; animation-delay: -.15s; }
.mpl-playlist__eq i:nth-child(3) { height: 10px; animation-delay: -.8s; }
@keyframes mpl-bounce { 0%, 100% { transform: scaleY(.35); } 50% { transform: scaleY(1); } }
.mpl-playlist__meta { min-width: 0; flex: 1; display: grid; }
.mpl-playlist__name {
font-size: 13.5px;
font-weight: 700;
color: #23282a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mpl-playlist__item.is-active .mpl-playlist__name { color: #1f7a4f; }
.mpl-playlist__artist { font-size: 11.5px; color: #8b958e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mpl-playlist__duration { flex: none; font-size: 11.5px; font-variant-numeric: tabular-nums; color: #9aa39c; }
.mpl-playlist__progress {
position: absolute;
left: 12px;
right: 12px;
bottom: 3px;
height: 2px;
border-radius: 2px;
background: rgba(47, 158, 107, .12);
display: none;
}
.mpl-playlist__item.is-active .mpl-playlist__progress { display: block; }
.mpl-playlist__progress-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 2px;
background: linear-gradient(90deg, #6cc79b, #2f9e6b);
}
@media (prefers-reduced-motion: reduce) {
.mpl-playlist__row { transition: none; }
.mpl-playlist__eq i { animation: none; }
}
【JavaScript】
// プレイリスト: 実音声は使わず、選択中の曲の進捗バーをタイマーで擬似的に進める
(() => {
const list = document.querySelector('.mpl-playlist__list');
if (!list) return; // null安全
const items = Array.from(list.querySelectorAll('.mpl-playlist__item'));
if (!items.length) return;
const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;
const TICK_MS = 200;
const SPEED = 5.5; // デモとして体感しやすい速さで擬似進行させる倍率
let activeIndex = items.findIndex((li) => li.classList.contains('is-active'));
if (activeIndex < 0) activeIndex = 0;
let current = 0;
let playing = false;
let timer = null;
function durationOf(li) { return parseFloat(li.dataset.duration) || 0; }
function fillOf(li) { return li.querySelector('.mpl-playlist__progress-fill'); }
function rowOf(li) { return li.querySelector('.mpl-playlist__row'); }
function renderActiveState() {
items.forEach((li, i) => {
const active = i === activeIndex;
li.classList.toggle('is-active', active);
li.classList.toggle('is-playing', active && playing);
const row = rowOf(li);
if (row) row.setAttribute('aria-current', String(active));
if (!active) {
const fill = fillOf(li);
if (fill) fill.style.width = '0%';
}
});
}
function renderProgress() {
const li = items[activeIndex];
const d = durationOf(li);
const fill = fillOf(li);
if (fill) fill.style.width = `${d ? Math.min(100, (current / d) * 100) : 0}%`;
}
function setPlaying(next) {
playing = next;
items[activeIndex].classList.toggle('is-playing', next);
clearInterval(timer);
if (next) {
timer = setInterval(() => {
current += (TICK_MS / 1000) * SPEED;
const d = durationOf(items[activeIndex]);
if (current >= d) { goTo(activeIndex + 1, true); return; }
renderProgress();
}, TICK_MS);
}
}
function goTo(index, keepPlaying) {
const len = items.length;
activeIndex = ((index % len) + len) % len;
current = 0;
renderActiveState();
renderProgress();
setPlaying(keepPlaying !== false);
}
items.forEach((li, i) => {
const row = rowOf(li);
if (!row) return;
row.addEventListener('click', () => {
if (i === activeIndex) { setPlaying(!playing); }
else { goTo(i, true); }
});
});
renderActiveState();
renderProgress();
// カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
if (!reduceMotion) setPlaying(true);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。