メッセージリアクション
バブルにカーソルを乗せる(またはタップする)とリアクションパレットが浮かび上がり、選んだ絵文字が集計チップとして積み上がっていきます。
ライブデモ
コード
HTML
<!-- メッセージリアクション: バブルにホバー(またはタップ)でリアクションパレットが出て、選ぶと絵文字集計チップが増える -->
<div class="stage">
<section class="cr-panel" aria-label="山口遥さんとのトーク">
<ul class="cr-list" id="crList">
<li class="cr-row cr-row--in">
<span class="cr-avatar" aria-hidden="true">遥</span>
<div class="cr-col">
<div class="cr-wrap" data-reactable>
<p class="cr-bubble">この写真めちゃくちゃ良い!テンション上がる🔥</p>
<button type="button" class="cr-trigger" data-reaction-trigger aria-label="リアクションを追加" aria-expanded="false">😊+</button>
<div class="cr-palette" data-palette role="menu" aria-label="リアクションを選ぶ">
<button type="button" class="cr-palette__item" data-emoji="👍" aria-label="いいね">👍</button>
<button type="button" class="cr-palette__item" data-emoji="❤️" aria-label="ハート">❤️</button>
<button type="button" class="cr-palette__item" data-emoji="😂" aria-label="笑い">😂</button>
<button type="button" class="cr-palette__item" data-emoji="😮" aria-label="驚き">😮</button>
<button type="button" class="cr-palette__item" data-emoji="🙏" aria-label="感謝">🙏</button>
</div>
</div>
<div class="cr-chips" data-chips>
<button type="button" class="cr-chip is-active" data-emoji="👍" data-count="2">👍 <span>2</span></button>
</div>
</div>
</li>
<li class="cr-row cr-row--out">
<div class="cr-col cr-col--out">
<div class="cr-wrap" data-reactable>
<p class="cr-bubble cr-bubble--out">でしょ、現地で見たときの色がそのまま出てる</p>
<button type="button" class="cr-trigger" data-reaction-trigger aria-label="リアクションを追加" aria-expanded="false">😊+</button>
<div class="cr-palette" data-palette role="menu" aria-label="リアクションを選ぶ">
<button type="button" class="cr-palette__item" data-emoji="👍" aria-label="いいね">👍</button>
<button type="button" class="cr-palette__item" data-emoji="❤️" aria-label="ハート">❤️</button>
<button type="button" class="cr-palette__item" data-emoji="😂" aria-label="笑い">😂</button>
<button type="button" class="cr-palette__item" data-emoji="😮" aria-label="驚き">😮</button>
<button type="button" class="cr-palette__item" data-emoji="🙏" aria-label="感謝">🙏</button>
</div>
</div>
<div class="cr-chips" data-chips></div>
</div>
</li>
<li class="cr-row cr-row--in">
<span class="cr-avatar" aria-hidden="true">遥</span>
<div class="cr-col">
<div class="cr-wrap" data-reactable>
<p class="cr-bubble">今度撮影のコツ教えてください🙏</p>
<button type="button" class="cr-trigger" data-reaction-trigger aria-label="リアクションを追加" aria-expanded="false">😊+</button>
<div class="cr-palette" data-palette role="menu" aria-label="リアクションを選ぶ">
<button type="button" class="cr-palette__item" data-emoji="👍" aria-label="いいね">👍</button>
<button type="button" class="cr-palette__item" data-emoji="❤️" aria-label="ハート">❤️</button>
<button type="button" class="cr-palette__item" data-emoji="😂" aria-label="笑い">😂</button>
<button type="button" class="cr-palette__item" data-emoji="😮" aria-label="驚き">😮</button>
<button type="button" class="cr-palette__item" data-emoji="🙏" aria-label="感謝">🙏</button>
</div>
</div>
<div class="cr-chips" data-chips></div>
</div>
</li>
</ul>
<p class="cr-hint">吹き出しにカーソルを乗せる(またはボタンをタップ)とリアクションを選べます</p>
</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% 8%, rgba(244, 114, 182, .14) 0%, transparent 42%),
radial-gradient(circle at 6% 92%, rgba(129, 140, 248, .1) 0%, transparent 45%),
#150e18;
color: #f4eefb;
overflow-x: hidden;
}
.cr-panel {
width: min(380px, 92vw);
border-radius: 22px;
padding: 22px 18px 18px;
background: rgba(255, 255, 255, .045);
border: 1px solid rgba(255, 255, 255, .09);
box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .75);
}
.cr-list {
list-style: none;
margin: 0 0 14px;
padding: 8px 4px 4px;
display: grid;
gap: 30px;
}
.cr-row { display: flex; align-items: flex-start; gap: 8px; }
.cr-row--out { justify-content: flex-end; }
.cr-avatar {
flex: none;
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
color: #38071f;
font-weight: 700;
font-size: 12px;
background: linear-gradient(135deg, #f9a8d4, #f472b6);
}
.cr-col { display: grid; gap: 6px; max-width: 78%; }
.cr-col--out { justify-items: end; }
.cr-wrap { position: relative; width: fit-content; }
.cr-bubble {
margin: 0;
padding: 10px 14px;
font-size: 13.5px;
line-height: 1.55;
border-radius: 16px;
border-bottom-left-radius: 5px;
background: rgba(255, 255, 255, .08);
border: 1px solid rgba(255, 255, 255, .07);
color: #f4eefb;
}
.cr-bubble--out {
background: linear-gradient(135deg, #f472b6, #db2777);
border-bottom-left-radius: 16px;
border-bottom-right-radius: 5px;
color: #fff;
box-shadow: 0 10px 22px -12px rgba(219, 39, 119, .7);
}
/* リアクション追加トリガー */
.cr-trigger {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, .18);
background: #221527;
color: #f4eefb;
font-size: 11px;
line-height: 1;
display: grid;
place-items: center;
cursor: pointer;
opacity: 0;
transform: scale(.85);
transition: opacity .18s ease, transform .18s ease;
}
.cr-wrap:hover .cr-trigger,
.cr-wrap:focus-within .cr-trigger,
.cr-wrap.is-open .cr-trigger {
opacity: 1;
transform: scale(1);
}
/* リアクションパレット */
.cr-palette {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 10px;
display: flex;
gap: 4px;
padding: 6px;
border-radius: 999px;
background: #221527;
border: 1px solid rgba(255, 255, 255, .12);
box-shadow: 0 16px 34px -16px rgba(0, 0, 0, .8);
opacity: 0;
transform: translateY(6px) scale(.92);
transform-origin: bottom left;
pointer-events: none;
transition: opacity .18s ease, transform .18s cubic-bezier(.22, 1, .36, 1);
z-index: 2;
}
.cr-row--out .cr-palette { left: auto; right: 0; transform-origin: bottom right; }
.cr-wrap:hover .cr-palette,
.cr-wrap:focus-within .cr-palette,
.cr-wrap.is-open .cr-palette {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.cr-palette__item {
font-size: 17px;
line-height: 1;
padding: 6px;
border: none;
border-radius: 50%;
background: transparent;
cursor: pointer;
transition: transform .15s cubic-bezier(.34, 1.56, .64, 1), background .15s ease;
}
.cr-palette__item:hover, .cr-palette__item:focus-visible {
background: rgba(255, 255, 255, .12);
transform: translateY(-3px) scale(1.2);
}
/* 集計チップ */
.cr-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cr-chips:empty { display: none; }
.cr-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font: inherit;
font-size: 11.5px;
font-weight: 600;
padding: 4px 9px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, .12);
background: rgba(255, 255, 255, .06);
color: rgba(244, 238, 251, .75);
cursor: pointer;
animation: cr-chip-in .25s cubic-bezier(.34, 1.56, .64, 1);
transition: background .15s ease, border-color .15s ease;
}
.cr-chip.is-active {
background: rgba(244, 114, 182, .18);
border-color: rgba(244, 114, 182, .55);
color: #fbcfe8;
}
.cr-chip:hover { background: rgba(255, 255, 255, .12); }
@keyframes cr-chip-in {
from { opacity: 0; transform: scale(.6); }
to { opacity: 1; transform: scale(1); }
}
.cr-hint {
margin: 0;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, .08);
font-size: 11px;
text-align: center;
color: rgba(244, 238, 251, .4);
}
@media (prefers-reduced-motion: reduce) {
.cr-trigger, .cr-palette, .cr-palette__item, .cr-chip { transition: none; animation: none; }
}
@media (max-width: 380px) {
.cr-panel { border-radius: 18px; padding: 18px 14px 14px; }
}
JavaScript
// メッセージリアクション: パレットの開閉と、絵文字ごとの集計チップの生成・増減をイベント委譲で処理
(() => {
const list = document.getElementById('crList');
if (!list) return; // null安全
const closeAllPalettes = (exceptWrap) => {
list.querySelectorAll('.cr-wrap.is-open').forEach((wrap) => {
if (wrap === exceptWrap) return;
wrap.classList.remove('is-open');
const trigger = wrap.querySelector('[data-reaction-trigger]');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
});
};
const findChip = (chipsEl, emoji) => {
const chips = chipsEl.querySelectorAll('[data-emoji]');
for (const chip of chips) {
if (chip.dataset.emoji === emoji) return chip;
}
return null;
};
const toggleReaction = (chipsEl, emoji) => {
if (!chipsEl || !emoji) return;
let chip = findChip(chipsEl, emoji);
if (chip && chip.classList.contains('is-active')) {
// 自分のリアクションを取り消す
const next = (parseInt(chip.dataset.count, 10) || 1) - 1;
if (next <= 0) {
chip.remove();
} else {
chip.dataset.count = String(next);
const span = chip.querySelector('span');
if (span) span.textContent = String(next);
chip.classList.remove('is-active');
}
return;
}
if (chip) {
// 既存チップに自分の分を追加
const next = (parseInt(chip.dataset.count, 10) || 0) + 1;
chip.dataset.count = String(next);
const span = chip.querySelector('span');
if (span) span.textContent = String(next);
chip.classList.add('is-active');
return;
}
// 新規チップを作成
chip = document.createElement('button');
chip.type = 'button';
chip.className = 'cr-chip is-active';
chip.dataset.emoji = emoji;
chip.dataset.count = '1';
chip.append(emoji + ' ');
const span = document.createElement('span');
span.textContent = '1';
chip.appendChild(span);
chipsEl.appendChild(chip);
};
list.addEventListener('click', (e) => {
// トリガーボタン: パレットの開閉(他は閉じる)
const trigger = e.target.closest('[data-reaction-trigger]');
if (trigger) {
const wrap = trigger.closest('[data-reactable]');
if (!wrap) return;
const willOpen = !wrap.classList.contains('is-open');
closeAllPalettes(willOpen ? wrap : null);
wrap.classList.toggle('is-open', willOpen);
trigger.setAttribute('aria-expanded', String(willOpen));
return;
}
// パレット内の絵文字 or 既存チップ: 集計を更新
const emojiBtn = e.target.closest('[data-emoji]');
if (emojiBtn) {
const col = emojiBtn.closest('.cr-col');
const chipsEl = col ? col.querySelector('[data-chips]') : null;
toggleReaction(chipsEl, emojiBtn.dataset.emoji);
if (emojiBtn.classList.contains('cr-palette__item')) {
const wrap = emojiBtn.closest('[data-reactable]');
if (wrap) {
wrap.classList.remove('is-open');
const t = wrap.querySelector('[data-reaction-trigger]');
if (t) t.setAttribute('aria-expanded', 'false');
}
}
}
});
// パレットの外側をクリックしたら閉じる(タッチ操作向け)
document.addEventListener('click', (e) => {
if (!list.contains(e.target)) closeAllPalettes(null);
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「メッセージリアクション」の効果を追加してください。
# 追加してほしい効果
メッセージリアクション(チャット & メディア)
バブルにカーソルを乗せる(またはタップする)とリアクションパレットが浮かび上がり、選んだ絵文字が集計チップとして積み上がっていきます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- メッセージリアクション: バブルにホバー(またはタップ)でリアクションパレットが出て、選ぶと絵文字集計チップが増える -->
<div class="stage">
<section class="cr-panel" aria-label="山口遥さんとのトーク">
<ul class="cr-list" id="crList">
<li class="cr-row cr-row--in">
<span class="cr-avatar" aria-hidden="true">遥</span>
<div class="cr-col">
<div class="cr-wrap" data-reactable>
<p class="cr-bubble">この写真めちゃくちゃ良い!テンション上がる🔥</p>
<button type="button" class="cr-trigger" data-reaction-trigger aria-label="リアクションを追加" aria-expanded="false">😊+</button>
<div class="cr-palette" data-palette role="menu" aria-label="リアクションを選ぶ">
<button type="button" class="cr-palette__item" data-emoji="👍" aria-label="いいね">👍</button>
<button type="button" class="cr-palette__item" data-emoji="❤️" aria-label="ハート">❤️</button>
<button type="button" class="cr-palette__item" data-emoji="😂" aria-label="笑い">😂</button>
<button type="button" class="cr-palette__item" data-emoji="😮" aria-label="驚き">😮</button>
<button type="button" class="cr-palette__item" data-emoji="🙏" aria-label="感謝">🙏</button>
</div>
</div>
<div class="cr-chips" data-chips>
<button type="button" class="cr-chip is-active" data-emoji="👍" data-count="2">👍 <span>2</span></button>
</div>
</div>
</li>
<li class="cr-row cr-row--out">
<div class="cr-col cr-col--out">
<div class="cr-wrap" data-reactable>
<p class="cr-bubble cr-bubble--out">でしょ、現地で見たときの色がそのまま出てる</p>
<button type="button" class="cr-trigger" data-reaction-trigger aria-label="リアクションを追加" aria-expanded="false">😊+</button>
<div class="cr-palette" data-palette role="menu" aria-label="リアクションを選ぶ">
<button type="button" class="cr-palette__item" data-emoji="👍" aria-label="いいね">👍</button>
<button type="button" class="cr-palette__item" data-emoji="❤️" aria-label="ハート">❤️</button>
<button type="button" class="cr-palette__item" data-emoji="😂" aria-label="笑い">😂</button>
<button type="button" class="cr-palette__item" data-emoji="😮" aria-label="驚き">😮</button>
<button type="button" class="cr-palette__item" data-emoji="🙏" aria-label="感謝">🙏</button>
</div>
</div>
<div class="cr-chips" data-chips></div>
</div>
</li>
<li class="cr-row cr-row--in">
<span class="cr-avatar" aria-hidden="true">遥</span>
<div class="cr-col">
<div class="cr-wrap" data-reactable>
<p class="cr-bubble">今度撮影のコツ教えてください🙏</p>
<button type="button" class="cr-trigger" data-reaction-trigger aria-label="リアクションを追加" aria-expanded="false">😊+</button>
<div class="cr-palette" data-palette role="menu" aria-label="リアクションを選ぶ">
<button type="button" class="cr-palette__item" data-emoji="👍" aria-label="いいね">👍</button>
<button type="button" class="cr-palette__item" data-emoji="❤️" aria-label="ハート">❤️</button>
<button type="button" class="cr-palette__item" data-emoji="😂" aria-label="笑い">😂</button>
<button type="button" class="cr-palette__item" data-emoji="😮" aria-label="驚き">😮</button>
<button type="button" class="cr-palette__item" data-emoji="🙏" aria-label="感謝">🙏</button>
</div>
</div>
<div class="cr-chips" data-chips></div>
</div>
</li>
</ul>
<p class="cr-hint">吹き出しにカーソルを乗せる(またはボタンをタップ)とリアクションを選べます</p>
</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% 8%, rgba(244, 114, 182, .14) 0%, transparent 42%),
radial-gradient(circle at 6% 92%, rgba(129, 140, 248, .1) 0%, transparent 45%),
#150e18;
color: #f4eefb;
overflow-x: hidden;
}
.cr-panel {
width: min(380px, 92vw);
border-radius: 22px;
padding: 22px 18px 18px;
background: rgba(255, 255, 255, .045);
border: 1px solid rgba(255, 255, 255, .09);
box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .75);
}
.cr-list {
list-style: none;
margin: 0 0 14px;
padding: 8px 4px 4px;
display: grid;
gap: 30px;
}
.cr-row { display: flex; align-items: flex-start; gap: 8px; }
.cr-row--out { justify-content: flex-end; }
.cr-avatar {
flex: none;
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
color: #38071f;
font-weight: 700;
font-size: 12px;
background: linear-gradient(135deg, #f9a8d4, #f472b6);
}
.cr-col { display: grid; gap: 6px; max-width: 78%; }
.cr-col--out { justify-items: end; }
.cr-wrap { position: relative; width: fit-content; }
.cr-bubble {
margin: 0;
padding: 10px 14px;
font-size: 13.5px;
line-height: 1.55;
border-radius: 16px;
border-bottom-left-radius: 5px;
background: rgba(255, 255, 255, .08);
border: 1px solid rgba(255, 255, 255, .07);
color: #f4eefb;
}
.cr-bubble--out {
background: linear-gradient(135deg, #f472b6, #db2777);
border-bottom-left-radius: 16px;
border-bottom-right-radius: 5px;
color: #fff;
box-shadow: 0 10px 22px -12px rgba(219, 39, 119, .7);
}
/* リアクション追加トリガー */
.cr-trigger {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, .18);
background: #221527;
color: #f4eefb;
font-size: 11px;
line-height: 1;
display: grid;
place-items: center;
cursor: pointer;
opacity: 0;
transform: scale(.85);
transition: opacity .18s ease, transform .18s ease;
}
.cr-wrap:hover .cr-trigger,
.cr-wrap:focus-within .cr-trigger,
.cr-wrap.is-open .cr-trigger {
opacity: 1;
transform: scale(1);
}
/* リアクションパレット */
.cr-palette {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 10px;
display: flex;
gap: 4px;
padding: 6px;
border-radius: 999px;
background: #221527;
border: 1px solid rgba(255, 255, 255, .12);
box-shadow: 0 16px 34px -16px rgba(0, 0, 0, .8);
opacity: 0;
transform: translateY(6px) scale(.92);
transform-origin: bottom left;
pointer-events: none;
transition: opacity .18s ease, transform .18s cubic-bezier(.22, 1, .36, 1);
z-index: 2;
}
.cr-row--out .cr-palette { left: auto; right: 0; transform-origin: bottom right; }
.cr-wrap:hover .cr-palette,
.cr-wrap:focus-within .cr-palette,
.cr-wrap.is-open .cr-palette {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.cr-palette__item {
font-size: 17px;
line-height: 1;
padding: 6px;
border: none;
border-radius: 50%;
background: transparent;
cursor: pointer;
transition: transform .15s cubic-bezier(.34, 1.56, .64, 1), background .15s ease;
}
.cr-palette__item:hover, .cr-palette__item:focus-visible {
background: rgba(255, 255, 255, .12);
transform: translateY(-3px) scale(1.2);
}
/* 集計チップ */
.cr-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cr-chips:empty { display: none; }
.cr-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font: inherit;
font-size: 11.5px;
font-weight: 600;
padding: 4px 9px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, .12);
background: rgba(255, 255, 255, .06);
color: rgba(244, 238, 251, .75);
cursor: pointer;
animation: cr-chip-in .25s cubic-bezier(.34, 1.56, .64, 1);
transition: background .15s ease, border-color .15s ease;
}
.cr-chip.is-active {
background: rgba(244, 114, 182, .18);
border-color: rgba(244, 114, 182, .55);
color: #fbcfe8;
}
.cr-chip:hover { background: rgba(255, 255, 255, .12); }
@keyframes cr-chip-in {
from { opacity: 0; transform: scale(.6); }
to { opacity: 1; transform: scale(1); }
}
.cr-hint {
margin: 0;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, .08);
font-size: 11px;
text-align: center;
color: rgba(244, 238, 251, .4);
}
@media (prefers-reduced-motion: reduce) {
.cr-trigger, .cr-palette, .cr-palette__item, .cr-chip { transition: none; animation: none; }
}
@media (max-width: 380px) {
.cr-panel { border-radius: 18px; padding: 18px 14px 14px; }
}
【JavaScript】
// メッセージリアクション: パレットの開閉と、絵文字ごとの集計チップの生成・増減をイベント委譲で処理
(() => {
const list = document.getElementById('crList');
if (!list) return; // null安全
const closeAllPalettes = (exceptWrap) => {
list.querySelectorAll('.cr-wrap.is-open').forEach((wrap) => {
if (wrap === exceptWrap) return;
wrap.classList.remove('is-open');
const trigger = wrap.querySelector('[data-reaction-trigger]');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
});
};
const findChip = (chipsEl, emoji) => {
const chips = chipsEl.querySelectorAll('[data-emoji]');
for (const chip of chips) {
if (chip.dataset.emoji === emoji) return chip;
}
return null;
};
const toggleReaction = (chipsEl, emoji) => {
if (!chipsEl || !emoji) return;
let chip = findChip(chipsEl, emoji);
if (chip && chip.classList.contains('is-active')) {
// 自分のリアクションを取り消す
const next = (parseInt(chip.dataset.count, 10) || 1) - 1;
if (next <= 0) {
chip.remove();
} else {
chip.dataset.count = String(next);
const span = chip.querySelector('span');
if (span) span.textContent = String(next);
chip.classList.remove('is-active');
}
return;
}
if (chip) {
// 既存チップに自分の分を追加
const next = (parseInt(chip.dataset.count, 10) || 0) + 1;
chip.dataset.count = String(next);
const span = chip.querySelector('span');
if (span) span.textContent = String(next);
chip.classList.add('is-active');
return;
}
// 新規チップを作成
chip = document.createElement('button');
chip.type = 'button';
chip.className = 'cr-chip is-active';
chip.dataset.emoji = emoji;
chip.dataset.count = '1';
chip.append(emoji + ' ');
const span = document.createElement('span');
span.textContent = '1';
chip.appendChild(span);
chipsEl.appendChild(chip);
};
list.addEventListener('click', (e) => {
// トリガーボタン: パレットの開閉(他は閉じる)
const trigger = e.target.closest('[data-reaction-trigger]');
if (trigger) {
const wrap = trigger.closest('[data-reactable]');
if (!wrap) return;
const willOpen = !wrap.classList.contains('is-open');
closeAllPalettes(willOpen ? wrap : null);
wrap.classList.toggle('is-open', willOpen);
trigger.setAttribute('aria-expanded', String(willOpen));
return;
}
// パレット内の絵文字 or 既存チップ: 集計を更新
const emojiBtn = e.target.closest('[data-emoji]');
if (emojiBtn) {
const col = emojiBtn.closest('.cr-col');
const chipsEl = col ? col.querySelector('[data-chips]') : null;
toggleReaction(chipsEl, emojiBtn.dataset.emoji);
if (emojiBtn.classList.contains('cr-palette__item')) {
const wrap = emojiBtn.closest('[data-reactable]');
if (wrap) {
wrap.classList.remove('is-open');
const t = wrap.querySelector('[data-reaction-trigger]');
if (t) t.setAttribute('aria-expanded', 'false');
}
}
}
});
// パレットの外側をクリックしたら閉じる(タッチ操作向け)
document.addEventListener('click', (e) => {
if (!list.contains(e.target)) closeAllPalettes(null);
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。