コメントスレッド
ネストした返信といいね・返信ボタンを備えたコメント欄。長く伸びた返信スレッドは折りたたんでおき、必要なときだけ矢印から展開できます。
ライブデモ
コード
HTML
<!-- コメントスレッド: ネスト返信、いいね/返信ボタン、長い返信の折りたたみを備えたコメント欄 -->
<div class="stage">
<section class="cct-panel" aria-label="コメントスレッド">
<p class="cct-title">コメント <span class="cct-title__count">4</span></p>
<ul class="cct-list" id="cctList">
<li class="cct-item" data-comment>
<div class="cct-row">
<span class="cct-avatar" style="--c1:#fca5a5;--c2:#f97316" aria-hidden="true">健</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">田村 健</strong><span class="cct-time">3時間前</span></p>
<p class="cct-text">このデザイン、余白の使い方がすごく綺麗ですね。参考にさせてください!</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">12</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<button type="button" class="cct-collapse" data-collapse-toggle aria-expanded="false">
<span class="cct-collapse__chevron" aria-hidden="true">▾</span>
3件の返信を表示
</button>
<div class="cct-collapsible" data-collapsible>
<ul class="cct-replies" data-replies>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#93c5fd;--c2:#3b82f6" aria-hidden="true">彩</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">中村 彩</strong><span class="cct-time">2時間前</span></p>
<p class="cct-text">わかります、グリッドの揃え方が丁寧ですよね。</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">4</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#fca5a5;--c2:#f97316" aria-hidden="true">健</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">田村 健</strong><span class="cct-time">2時間前</span></p>
<p class="cct-text">彩さんもありがとう、今度配色も見てほしいです</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</li>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#86efac;--c2:#059669" aria-hidden="true">遼</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">小林 遼</strong><span class="cct-time">1時間前</span></p>
<p class="cct-text">配色ももう少し見てみたいです、続編待ってます</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">2</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies></ul>
</div>
</div>
</li>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#c4b5fd;--c2:#7c3aed" aria-hidden="true">舞</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">松本 舞</strong><span class="cct-time">40分前</span></p>
<p class="cct-text">保存しました、資料作りの参考にします!</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">1</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies></ul>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</li>
<li class="cct-item" data-comment>
<div class="cct-row">
<span class="cct-avatar" style="--c1:#7dd3fc;--c2:#0ea5e9" aria-hidden="true">葵</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">石田 葵</strong><span class="cct-time">10分前</span></p>
<p class="cct-text">配布素材はどこかで公開予定ですか?楽しみにしています。</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">0</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies></ul>
</div>
</div>
</li>
</ul>
</section>
</div>
CSS
/* コメントスレッド */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px;
font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 90% 6%, rgba(251, 146, 60, .13) 0%, transparent 42%),
radial-gradient(circle at 6% 94%, rgba(56, 189, 248, .1) 0%, transparent 45%),
#14100b;
color: #f2ece4;
}
.cct-panel {
width: min(420px, 92vw);
max-height: 82vh;
overflow-y: auto;
border-radius: 20px;
padding: 20px 18px 22px;
background: rgba(255, 255, 255, .04);
border: 1px solid rgba(255, 255, 255, .08);
box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .75);
}
.cct-title {
margin: 0 0 14px;
font-size: 14.5px;
font-weight: 800;
}
.cct-title__count { color: rgba(242, 236, 228, .5); font-weight: 600; }
.cct-list, .cct-replies {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 16px;
}
.cct-list { gap: 20px; }
.cct-row { display: flex; gap: 10px; align-items: flex-start; }
.cct-avatar {
--c1: #fca5a5;
--c2: #f97316;
flex: none;
width: 34px;
height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
color: #1c1206;
font-weight: 800;
font-size: 12.5px;
background: linear-gradient(135deg, var(--c1), var(--c2));
}
.cct-avatar--sm { width: 28px; height: 28px; font-size: 11px; }
.cct-body { flex: 1; min-width: 0; }
.cct-meta {
margin: 0 0 4px;
font-size: 12.5px;
display: flex;
align-items: baseline;
gap: 8px;
}
.cct-name { color: #f2ece4; }
.cct-time { color: rgba(242, 236, 228, .42); font-size: 11px; font-weight: 400; }
.cct-text {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.7;
color: rgba(242, 236, 228, .88);
}
.cct-actions { display: flex; align-items: center; gap: 14px; }
.cct-like {
display: inline-flex;
align-items: center;
gap: 5px;
font: inherit;
font-size: 12px;
color: rgba(242, 236, 228, .55);
background: none;
border: none;
padding: 2px 0;
cursor: pointer;
transition: color .15s ease, transform .15s ease;
}
.cct-like:hover { color: #fb7185; }
.cct-like__icon { font-size: 14px; transition: transform .25s cubic-bezier(.34, 1.56, .64, 1); }
.cct-like.is-liked { color: #fb7185; }
.cct-like.is-liked .cct-like__icon { transform: scale(1.15); }
.cct-reply-btn {
font: inherit;
font-size: 12px;
font-weight: 700;
color: rgba(242, 236, 228, .55);
background: none;
border: none;
padding: 0;
cursor: pointer;
transition: color .15s ease;
}
.cct-reply-btn:hover { color: #fdba74; }
/* 返信フォーム */
.cct-reply-form {
display: flex;
gap: 8px;
margin: 10px 0 0;
}
.cct-reply-form[hidden] { display: none; }
.cct-reply-input {
flex: 1;
min-width: 0;
padding: 7px 12px;
font: inherit;
font-size: 12.5px;
color: #f2ece4;
background: rgba(255, 255, 255, .06);
border: 1px solid rgba(255, 255, 255, .1);
border-radius: 999px;
}
.cct-reply-input:focus { outline: none; border-color: rgba(251, 146, 60, .5); }
.cct-reply-input::placeholder { color: rgba(242, 236, 228, .35); }
.cct-reply-submit {
flex: none;
font: inherit;
font-size: 12px;
font-weight: 700;
padding: 7px 14px;
border: none;
border-radius: 999px;
color: #1c1206;
background: linear-gradient(135deg, #fdba74, #f97316);
cursor: pointer;
transition: transform .15s ease;
}
.cct-reply-submit:hover { transform: translateY(-1px); }
.cct-reply-submit:active { transform: scale(.95); }
/* 折りたたみトグル */
.cct-collapse {
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 10px;
font: inherit;
font-size: 12px;
font-weight: 700;
color: #7dd3fc;
background: none;
border: none;
padding: 0;
cursor: pointer;
}
.cct-collapse__chevron { display: inline-block; transition: transform .3s cubic-bezier(.22, 1, .36, 1); }
.cct-collapse[aria-expanded="true"] .cct-collapse__chevron { transform: rotate(-180deg); }
/* grid-template-rows トリックで高さ自動アニメーション */
.cct-collapsible {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows .4s cubic-bezier(.22, 1, .36, 1);
}
.cct-collapsible.is-open { grid-template-rows: 1fr; }
.cct-collapsible > .cct-replies {
overflow: hidden;
min-height: 0;
margin-top: 14px;
padding-left: 14px;
border-left: 2px solid rgba(255, 255, 255, .08);
}
.cct-item--reply .cct-replies {
margin-top: 12px;
padding-left: 12px;
border-left: 2px solid rgba(255, 255, 255, .06);
}
@media (prefers-reduced-motion: reduce) {
.cct-collapsible, .cct-collapse__chevron, .cct-like__icon, .cct-reply-submit {
transition: none;
}
}
@media (max-width: 400px) {
.cct-panel { padding: 16px 14px 18px; }
}
JavaScript
// コメントスレッド: いいねの切替、返信フォームの開閉と投稿、返信の折りたたみをイベント委譲で処理
(() => {
const list = document.getElementById('cctList');
if (!list) return; // null安全
const AVATAR_NAME = 'あなた';
const AVATAR_COLORS = ['#93c5fd', '#3b82f6'];
const closestDirectReplies = (body) => {
if (!body) return null;
return (
body.querySelector(':scope > .cct-collapsible > [data-replies]') ||
body.querySelector(':scope > [data-replies]')
);
};
list.addEventListener('click', (e) => {
// いいねの切替
const likeBtn = e.target.closest('[data-like]');
if (likeBtn) {
const countEl = likeBtn.querySelector('.cct-like__count');
const iconEl = likeBtn.querySelector('.cct-like__icon');
const liked = likeBtn.classList.toggle('is-liked');
likeBtn.setAttribute('aria-pressed', String(liked));
if (countEl) {
const current = parseInt(countEl.textContent, 10) || 0;
countEl.textContent = String(liked ? current + 1 : Math.max(0, current - 1));
}
if (iconEl) iconEl.textContent = liked ? '♥' : '♡';
return;
}
// 返信フォームの開閉
const replyToggle = e.target.closest('[data-reply-toggle]');
if (replyToggle) {
const body = replyToggle.closest('.cct-body');
const formEl = body ? body.querySelector(':scope > [data-reply-form]') : null;
if (!formEl) return;
const willShow = formEl.hasAttribute('hidden');
if (willShow) {
formEl.removeAttribute('hidden');
const input = formEl.querySelector('.cct-reply-input');
if (input) input.focus();
} else {
formEl.setAttribute('hidden', '');
}
return;
}
// 長いスレッドの折りたたみ
const collapseToggle = e.target.closest('[data-collapse-toggle]');
if (collapseToggle) {
const body = collapseToggle.closest('.cct-body');
const box = body ? body.querySelector(':scope > .cct-collapsible') : null;
if (!box) return;
const opening = !box.classList.contains('is-open');
box.classList.toggle('is-open', opening);
collapseToggle.setAttribute('aria-expanded', String(opening));
const label = collapseToggle.textContent.replace(/^\s*▾?\s*/, '').trim();
collapseToggle.innerHTML = '';
const chevronSpan = document.createElement('span');
chevronSpan.className = 'cct-collapse__chevron';
chevronSpan.setAttribute('aria-hidden', 'true');
chevronSpan.textContent = '▾';
collapseToggle.appendChild(chevronSpan);
collapseToggle.appendChild(document.createTextNode(
opening ? label.replace('を表示', 'を隠す') : label.replace('を隠す', 'を表示')
));
}
});
list.addEventListener('submit', (e) => {
const formEl = e.target.closest('[data-reply-form]');
if (!formEl) return;
e.preventDefault();
const input = formEl.querySelector('.cct-reply-input');
if (!input) return;
const text = input.value.trim();
if (!text) return;
const body = formEl.closest('.cct-body');
const repliesEl = closestDirectReplies(body);
if (!repliesEl) return;
const li = document.createElement('li');
li.className = 'cct-item cct-item--reply';
li.setAttribute('data-comment', '');
li.innerHTML =
'<div class="cct-row">' +
'<span class="cct-avatar cct-avatar--sm" aria-hidden="true">あ</span>' +
'<div class="cct-body">' +
'<p class="cct-meta"><strong class="cct-name"></strong><span class="cct-time">たった今</span></p>' +
'<p class="cct-text"></p>' +
'</div></div>';
li.querySelector('.cct-avatar').style.setProperty('--c1', AVATAR_COLORS[0]);
li.querySelector('.cct-avatar').style.setProperty('--c2', AVATAR_COLORS[1]);
li.querySelector('.cct-name').textContent = AVATAR_NAME;
li.querySelector('.cct-text').textContent = text;
repliesEl.appendChild(li);
// 折りたたみ中だった場合は投稿した返信が見えるよう開く
const box = repliesEl.closest('.cct-collapsible');
if (box) {
box.classList.add('is-open');
const toggleBtn = body.querySelector(':scope > [data-collapse-toggle]');
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'true');
}
input.value = '';
formEl.setAttribute('hidden', '');
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「コメントスレッド」の効果を追加してください。
# 追加してほしい効果
コメントスレッド(チャット & メディア)
ネストした返信といいね・返信ボタンを備えたコメント欄。長く伸びた返信スレッドは折りたたんでおき、必要なときだけ矢印から展開できます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- コメントスレッド: ネスト返信、いいね/返信ボタン、長い返信の折りたたみを備えたコメント欄 -->
<div class="stage">
<section class="cct-panel" aria-label="コメントスレッド">
<p class="cct-title">コメント <span class="cct-title__count">4</span></p>
<ul class="cct-list" id="cctList">
<li class="cct-item" data-comment>
<div class="cct-row">
<span class="cct-avatar" style="--c1:#fca5a5;--c2:#f97316" aria-hidden="true">健</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">田村 健</strong><span class="cct-time">3時間前</span></p>
<p class="cct-text">このデザイン、余白の使い方がすごく綺麗ですね。参考にさせてください!</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">12</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<button type="button" class="cct-collapse" data-collapse-toggle aria-expanded="false">
<span class="cct-collapse__chevron" aria-hidden="true">▾</span>
3件の返信を表示
</button>
<div class="cct-collapsible" data-collapsible>
<ul class="cct-replies" data-replies>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#93c5fd;--c2:#3b82f6" aria-hidden="true">彩</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">中村 彩</strong><span class="cct-time">2時間前</span></p>
<p class="cct-text">わかります、グリッドの揃え方が丁寧ですよね。</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">4</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#fca5a5;--c2:#f97316" aria-hidden="true">健</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">田村 健</strong><span class="cct-time">2時間前</span></p>
<p class="cct-text">彩さんもありがとう、今度配色も見てほしいです</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</li>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#86efac;--c2:#059669" aria-hidden="true">遼</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">小林 遼</strong><span class="cct-time">1時間前</span></p>
<p class="cct-text">配色ももう少し見てみたいです、続編待ってます</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">2</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies></ul>
</div>
</div>
</li>
<li class="cct-item cct-item--reply" data-comment>
<div class="cct-row">
<span class="cct-avatar cct-avatar--sm" style="--c1:#c4b5fd;--c2:#7c3aed" aria-hidden="true">舞</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">松本 舞</strong><span class="cct-time">40分前</span></p>
<p class="cct-text">保存しました、資料作りの参考にします!</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">1</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies></ul>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</li>
<li class="cct-item" data-comment>
<div class="cct-row">
<span class="cct-avatar" style="--c1:#7dd3fc;--c2:#0ea5e9" aria-hidden="true">葵</span>
<div class="cct-body">
<p class="cct-meta"><strong class="cct-name">石田 葵</strong><span class="cct-time">10分前</span></p>
<p class="cct-text">配布素材はどこかで公開予定ですか?楽しみにしています。</p>
<div class="cct-actions">
<button type="button" class="cct-like" data-like aria-pressed="false">
<span class="cct-like__icon" aria-hidden="true">♡</span>
<span class="cct-like__count">0</span>
</button>
<button type="button" class="cct-reply-btn" data-reply-toggle>返信</button>
</div>
<form class="cct-reply-form" data-reply-form hidden>
<input type="text" class="cct-reply-input" placeholder="返信を入力…" aria-label="返信を入力">
<button type="submit" class="cct-reply-submit">送信</button>
</form>
<ul class="cct-replies" data-replies></ul>
</div>
</div>
</li>
</ul>
</section>
</div>
【CSS】
/* コメントスレッド */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px;
font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 90% 6%, rgba(251, 146, 60, .13) 0%, transparent 42%),
radial-gradient(circle at 6% 94%, rgba(56, 189, 248, .1) 0%, transparent 45%),
#14100b;
color: #f2ece4;
}
.cct-panel {
width: min(420px, 92vw);
max-height: 82vh;
overflow-y: auto;
border-radius: 20px;
padding: 20px 18px 22px;
background: rgba(255, 255, 255, .04);
border: 1px solid rgba(255, 255, 255, .08);
box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .75);
}
.cct-title {
margin: 0 0 14px;
font-size: 14.5px;
font-weight: 800;
}
.cct-title__count { color: rgba(242, 236, 228, .5); font-weight: 600; }
.cct-list, .cct-replies {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 16px;
}
.cct-list { gap: 20px; }
.cct-row { display: flex; gap: 10px; align-items: flex-start; }
.cct-avatar {
--c1: #fca5a5;
--c2: #f97316;
flex: none;
width: 34px;
height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
color: #1c1206;
font-weight: 800;
font-size: 12.5px;
background: linear-gradient(135deg, var(--c1), var(--c2));
}
.cct-avatar--sm { width: 28px; height: 28px; font-size: 11px; }
.cct-body { flex: 1; min-width: 0; }
.cct-meta {
margin: 0 0 4px;
font-size: 12.5px;
display: flex;
align-items: baseline;
gap: 8px;
}
.cct-name { color: #f2ece4; }
.cct-time { color: rgba(242, 236, 228, .42); font-size: 11px; font-weight: 400; }
.cct-text {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.7;
color: rgba(242, 236, 228, .88);
}
.cct-actions { display: flex; align-items: center; gap: 14px; }
.cct-like {
display: inline-flex;
align-items: center;
gap: 5px;
font: inherit;
font-size: 12px;
color: rgba(242, 236, 228, .55);
background: none;
border: none;
padding: 2px 0;
cursor: pointer;
transition: color .15s ease, transform .15s ease;
}
.cct-like:hover { color: #fb7185; }
.cct-like__icon { font-size: 14px; transition: transform .25s cubic-bezier(.34, 1.56, .64, 1); }
.cct-like.is-liked { color: #fb7185; }
.cct-like.is-liked .cct-like__icon { transform: scale(1.15); }
.cct-reply-btn {
font: inherit;
font-size: 12px;
font-weight: 700;
color: rgba(242, 236, 228, .55);
background: none;
border: none;
padding: 0;
cursor: pointer;
transition: color .15s ease;
}
.cct-reply-btn:hover { color: #fdba74; }
/* 返信フォーム */
.cct-reply-form {
display: flex;
gap: 8px;
margin: 10px 0 0;
}
.cct-reply-form[hidden] { display: none; }
.cct-reply-input {
flex: 1;
min-width: 0;
padding: 7px 12px;
font: inherit;
font-size: 12.5px;
color: #f2ece4;
background: rgba(255, 255, 255, .06);
border: 1px solid rgba(255, 255, 255, .1);
border-radius: 999px;
}
.cct-reply-input:focus { outline: none; border-color: rgba(251, 146, 60, .5); }
.cct-reply-input::placeholder { color: rgba(242, 236, 228, .35); }
.cct-reply-submit {
flex: none;
font: inherit;
font-size: 12px;
font-weight: 700;
padding: 7px 14px;
border: none;
border-radius: 999px;
color: #1c1206;
background: linear-gradient(135deg, #fdba74, #f97316);
cursor: pointer;
transition: transform .15s ease;
}
.cct-reply-submit:hover { transform: translateY(-1px); }
.cct-reply-submit:active { transform: scale(.95); }
/* 折りたたみトグル */
.cct-collapse {
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 10px;
font: inherit;
font-size: 12px;
font-weight: 700;
color: #7dd3fc;
background: none;
border: none;
padding: 0;
cursor: pointer;
}
.cct-collapse__chevron { display: inline-block; transition: transform .3s cubic-bezier(.22, 1, .36, 1); }
.cct-collapse[aria-expanded="true"] .cct-collapse__chevron { transform: rotate(-180deg); }
/* grid-template-rows トリックで高さ自動アニメーション */
.cct-collapsible {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows .4s cubic-bezier(.22, 1, .36, 1);
}
.cct-collapsible.is-open { grid-template-rows: 1fr; }
.cct-collapsible > .cct-replies {
overflow: hidden;
min-height: 0;
margin-top: 14px;
padding-left: 14px;
border-left: 2px solid rgba(255, 255, 255, .08);
}
.cct-item--reply .cct-replies {
margin-top: 12px;
padding-left: 12px;
border-left: 2px solid rgba(255, 255, 255, .06);
}
@media (prefers-reduced-motion: reduce) {
.cct-collapsible, .cct-collapse__chevron, .cct-like__icon, .cct-reply-submit {
transition: none;
}
}
@media (max-width: 400px) {
.cct-panel { padding: 16px 14px 18px; }
}
【JavaScript】
// コメントスレッド: いいねの切替、返信フォームの開閉と投稿、返信の折りたたみをイベント委譲で処理
(() => {
const list = document.getElementById('cctList');
if (!list) return; // null安全
const AVATAR_NAME = 'あなた';
const AVATAR_COLORS = ['#93c5fd', '#3b82f6'];
const closestDirectReplies = (body) => {
if (!body) return null;
return (
body.querySelector(':scope > .cct-collapsible > [data-replies]') ||
body.querySelector(':scope > [data-replies]')
);
};
list.addEventListener('click', (e) => {
// いいねの切替
const likeBtn = e.target.closest('[data-like]');
if (likeBtn) {
const countEl = likeBtn.querySelector('.cct-like__count');
const iconEl = likeBtn.querySelector('.cct-like__icon');
const liked = likeBtn.classList.toggle('is-liked');
likeBtn.setAttribute('aria-pressed', String(liked));
if (countEl) {
const current = parseInt(countEl.textContent, 10) || 0;
countEl.textContent = String(liked ? current + 1 : Math.max(0, current - 1));
}
if (iconEl) iconEl.textContent = liked ? '♥' : '♡';
return;
}
// 返信フォームの開閉
const replyToggle = e.target.closest('[data-reply-toggle]');
if (replyToggle) {
const body = replyToggle.closest('.cct-body');
const formEl = body ? body.querySelector(':scope > [data-reply-form]') : null;
if (!formEl) return;
const willShow = formEl.hasAttribute('hidden');
if (willShow) {
formEl.removeAttribute('hidden');
const input = formEl.querySelector('.cct-reply-input');
if (input) input.focus();
} else {
formEl.setAttribute('hidden', '');
}
return;
}
// 長いスレッドの折りたたみ
const collapseToggle = e.target.closest('[data-collapse-toggle]');
if (collapseToggle) {
const body = collapseToggle.closest('.cct-body');
const box = body ? body.querySelector(':scope > .cct-collapsible') : null;
if (!box) return;
const opening = !box.classList.contains('is-open');
box.classList.toggle('is-open', opening);
collapseToggle.setAttribute('aria-expanded', String(opening));
const label = collapseToggle.textContent.replace(/^\s*▾?\s*/, '').trim();
collapseToggle.innerHTML = '';
const chevronSpan = document.createElement('span');
chevronSpan.className = 'cct-collapse__chevron';
chevronSpan.setAttribute('aria-hidden', 'true');
chevronSpan.textContent = '▾';
collapseToggle.appendChild(chevronSpan);
collapseToggle.appendChild(document.createTextNode(
opening ? label.replace('を表示', 'を隠す') : label.replace('を隠す', 'を表示')
));
}
});
list.addEventListener('submit', (e) => {
const formEl = e.target.closest('[data-reply-form]');
if (!formEl) return;
e.preventDefault();
const input = formEl.querySelector('.cct-reply-input');
if (!input) return;
const text = input.value.trim();
if (!text) return;
const body = formEl.closest('.cct-body');
const repliesEl = closestDirectReplies(body);
if (!repliesEl) return;
const li = document.createElement('li');
li.className = 'cct-item cct-item--reply';
li.setAttribute('data-comment', '');
li.innerHTML =
'<div class="cct-row">' +
'<span class="cct-avatar cct-avatar--sm" aria-hidden="true">あ</span>' +
'<div class="cct-body">' +
'<p class="cct-meta"><strong class="cct-name"></strong><span class="cct-time">たった今</span></p>' +
'<p class="cct-text"></p>' +
'</div></div>';
li.querySelector('.cct-avatar').style.setProperty('--c1', AVATAR_COLORS[0]);
li.querySelector('.cct-avatar').style.setProperty('--c2', AVATAR_COLORS[1]);
li.querySelector('.cct-name').textContent = AVATAR_NAME;
li.querySelector('.cct-text').textContent = text;
repliesEl.appendChild(li);
// 折りたたみ中だった場合は投稿した返信が見えるよう開く
const box = repliesEl.closest('.cct-collapsible');
if (box) {
box.classList.add('is-open');
const toggleBtn = body.querySelector(':scope > [data-collapse-toggle]');
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'true');
}
input.value = '';
formEl.setAttribute('hidden', '');
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。