ユーザーメニュー
アバターをクリックすると、プロフィールや設定・テーマ切り替え・ログアウトをまとめたアカウントメニューが上向きに展開します。テーマはクリックのたびに切り替わります。
ライブデモ
コード
HTML
<!-- ユーザーメニュー: アバターをクリックすると、上向きの矢印が示す通りアカウントメニューが上に開く -->
<div class="dum-stage">
<div class="dum-card">
<p class="dum-card__label">ワークスペース</p>
<p class="dum-card__name">Sakura Studio</p>
<div class="dum-divider" aria-hidden="true"></div>
<div class="dum-anchor">
<button type="button" class="dum-trigger" id="dumTrigger" aria-haspopup="true" aria-expanded="false" aria-controls="dumMenu">
<span class="dum-trigger__avatar" aria-hidden="true">椿</span>
<span class="dum-trigger__text">
<span class="dum-trigger__name">椿 このは</span>
<span class="dum-trigger__role">オーナー</span>
</span>
<span class="dum-trigger__arrow" aria-hidden="true">▴</span>
</button>
<ul class="dum-menu" id="dumMenu" role="menu" aria-labelledby="dumTrigger" hidden>
<li class="dum-menu__header" role="none">
<span class="dum-menu__avatar" aria-hidden="true">椿</span>
<span class="dum-menu__who">
<span class="dum-menu__name">椿 このは</span>
<span class="dum-menu__mail">konoha@example.com</span>
</span>
</li>
<li class="dum-sep" role="separator"></li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" data-action="profile">
<span class="dum-item__icon" aria-hidden="true">👤</span>
<span class="dum-item__label">プロフィール</span>
</button>
</li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" data-action="settings">
<span class="dum-item__icon" aria-hidden="true">⚙</span>
<span class="dum-item__label">設定</span>
</button>
</li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" id="dumThemeBtn">
<span class="dum-item__icon" aria-hidden="true" id="dumThemeIcon">☀</span>
<span class="dum-item__label">テーマ:<span id="dumThemeLabel">ライト</span></span>
</button>
</li>
<li class="dum-sep" role="separator"></li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" data-action="logout">
<span class="dum-item__icon" aria-hidden="true">⏻</span>
<span class="dum-item__label">ログアウト</span>
</button>
</li>
</ul>
</div>
<p class="dum-hint" id="dumStatus" aria-live="polite"></p>
</div>
</div>
CSS
/* パープル×ピンクの淡いグラデーションを背景にした、アバター起点のアカウントメニュー */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background:
radial-gradient(circle at 15% 15%, rgba(168, 85, 247, .12) 0%, transparent 45%),
radial-gradient(circle at 85% 15%, rgba(236, 72, 153, .10) 0%, transparent 45%),
#f7f5fb;
color: #241f2e;
}
.dum-stage { width: 100%; max-width: 300px; }
.dum-card {
width: 100%;
padding: 18px 16px 16px;
background: #fff;
border: 1px solid #ece7f5;
border-radius: 18px;
box-shadow: 0 22px 46px -24px rgba(88, 28, 135, .28);
display: flex;
flex-direction: column;
gap: 4px;
}
.dum-card__label { margin: 0; font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #a78bc4; }
.dum-card__name { margin: 0 0 10px; font-size: 15px; font-weight: 700; color: #241f2e; }
.dum-divider { height: 1px; background: #efe9f7; margin: 0 0 12px; }
.dum-anchor { position: relative; }
.dum-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
font: inherit;
text-align: left;
background: none;
border: none;
border-radius: 12px;
cursor: pointer;
transition: background .16s ease;
}
.dum-trigger:hover { background: #f7f3fb; }
.dum-trigger:focus-visible { outline: 2px solid #a855f7; outline-offset: 2px; }
.dum-trigger[aria-expanded="true"] { background: #f1eafb; }
.dum-trigger__avatar {
flex: none;
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
color: #fff;
background: linear-gradient(145deg, #a855f7, #ec4899);
}
.dum-trigger__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dum-trigger__name { font-size: 13px; font-weight: 700; color: #241f2e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dum-trigger__role { font-size: 11px; color: #9c93ab; }
.dum-trigger__arrow { flex: none; font-size: 11px; color: #a78bc4; transition: transform .2s ease, color .2s ease; }
.dum-trigger[aria-expanded="true"] .dum-trigger__arrow { transform: translateY(-2px); color: #7c3aed; }
.dum-menu {
position: absolute;
z-index: 10;
left: 0;
right: 0;
bottom: calc(100% + 10px);
margin: 0;
padding: 8px;
list-style: none;
background: #fff;
border: 1px solid #ece7f5;
border-radius: 16px;
box-shadow: 0 26px 54px -18px rgba(88, 28, 135, .35);
transform-origin: bottom center;
opacity: 0;
transform: translateY(6px) scale(.97);
transition: opacity .16s ease, transform .16s ease;
}
.dum-menu[hidden] { display: none; }
.dum-menu.is-open { opacity: 1; transform: translateY(0) scale(1); }
.dum-menu__header { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; }
.dum-menu__avatar {
flex: none;
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 14px;
font-weight: 700;
color: #fff;
background: linear-gradient(145deg, #a855f7, #ec4899);
}
.dum-menu__who { display: flex; flex-direction: column; min-width: 0; }
.dum-menu__name { font-size: 13px; font-weight: 700; color: #241f2e; }
.dum-menu__mail { font-size: 11px; color: #9c93ab; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dum-sep { height: 1px; margin: 4px 4px; background: #efe9f7; }
.dum-item {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 9px 8px;
font: inherit;
font-size: 13px;
color: #362f42;
background: none;
border: none;
border-radius: 10px;
text-align: left;
cursor: pointer;
transition: background .13s ease;
}
.dum-item:hover,
.dum-item.is-active { background: #f7f3fb; }
.dum-item:focus-visible { outline: 2px solid #a855f7; outline-offset: -2px; }
.dum-item__icon { flex: none; width: 18px; text-align: center; font-size: 13px; display: inline-block; }
.dum-item__label { flex: 1; }
#dumThemeIcon.is-cycled { animation: dumSpin .35s ease; }
@keyframes dumSpin {
from { transform: rotate(-40deg) scale(.7); opacity: .3; }
to { transform: rotate(0) scale(1); opacity: 1; }
}
.dum-hint { margin: 10px 0 0; min-height: 1.4em; font-size: 11.5px; color: #9c93ab; text-align: center; }
@media (prefers-reduced-motion: reduce) {
.dum-menu, .dum-trigger__arrow, #dumThemeIcon { transition: none; }
#dumThemeIcon.is-cycled { animation: none; }
}
JavaScript
// ユーザーメニュー: アバターをクリックすると上向きにアカウントメニューが開く。テーマ項目はクリックのたびに循環する
(() => {
const stage = document.querySelector('.dum-stage');
const trigger = document.getElementById('dumTrigger');
const menu = document.getElementById('dumMenu');
const status = document.getElementById('dumStatus');
const themeBtn = document.getElementById('dumThemeBtn');
const themeIcon = document.getElementById('dumThemeIcon');
const themeLabel = document.getElementById('dumThemeLabel');
if (!stage || !trigger || !menu || !status) return;
const items = Array.from(menu.querySelectorAll('.dum-item'));
if (!items.length) return;
const isOpen = () => trigger.getAttribute('aria-expanded') === 'true';
const setActive = (index) => {
items.forEach((it, i) => it.classList.toggle('is-active', i === index));
};
const open = () => {
menu.hidden = false;
void menu.offsetHeight; // 強制リフローでトランジションを確実に発火させる
menu.classList.add('is-open');
trigger.setAttribute('aria-expanded', 'true');
setActive(0);
items[0].focus();
};
const close = (focusTrigger) => {
menu.hidden = true;
menu.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
setActive(-1);
if (focusTrigger) trigger.focus();
};
trigger.addEventListener('click', () => {
if (isOpen()) close(false); else open();
});
trigger.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
open();
}
});
const focusByIndex = (index) => {
const len = items.length;
const i = ((index % len) + len) % len;
setActive(i);
items[i].focus();
};
menu.addEventListener('keydown', (e) => {
const current = items.indexOf(document.activeElement);
switch (e.key) {
// メニューは上に開くため、下キーで先頭側へ・上キーで末尾側へと直感に合わせて動かす
case 'ArrowDown': e.preventDefault(); focusByIndex(current + 1); break;
case 'ArrowUp': e.preventDefault(); focusByIndex(current - 1); break;
case 'Home': e.preventDefault(); focusByIndex(0); break;
case 'End': e.preventDefault(); focusByIndex(items.length - 1); break;
case 'Escape': e.preventDefault(); close(true); break;
case 'Tab': close(false); break;
default: break;
}
});
const THEMES = [
{ label: 'ライト', icon: '☀' },
{ label: 'ダーク', icon: '☾' },
{ label: 'システム', icon: '◐' },
];
let themeIndex = 0;
const cycleTheme = () => {
themeIndex = (themeIndex + 1) % THEMES.length;
const t = THEMES[themeIndex];
if (themeLabel) themeLabel.textContent = t.label;
if (themeIcon) {
themeIcon.textContent = t.icon;
themeIcon.classList.remove('is-cycled');
void themeIcon.offsetWidth; // 強制リフローで連続クリックでもアニメを再発火させる
themeIcon.classList.add('is-cycled');
}
status.textContent = `テーマを${t.label}に変更しました`;
};
items.forEach((item, index) => {
item.addEventListener('click', () => {
if (item === themeBtn) {
cycleTheme(); // テーマは開いたまま連続で切り替えられるようにする
return;
}
const label = item.querySelector('.dum-item__label');
const text = label ? label.textContent.trim() : item.textContent.trim();
status.textContent = `「${text}」を実行しました`;
close(true);
});
item.addEventListener('mouseenter', () => setActive(index));
});
document.addEventListener('click', (e) => {
if (isOpen() && !stage.contains(e.target)) close(false);
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ユーザーメニュー」の効果を追加してください。
# 追加してほしい効果
ユーザーメニュー(ツールチップ & メニュー)
アバターをクリックすると、プロフィールや設定・テーマ切り替え・ログアウトをまとめたアカウントメニューが上向きに展開します。テーマはクリックのたびに切り替わります。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ユーザーメニュー: アバターをクリックすると、上向きの矢印が示す通りアカウントメニューが上に開く -->
<div class="dum-stage">
<div class="dum-card">
<p class="dum-card__label">ワークスペース</p>
<p class="dum-card__name">Sakura Studio</p>
<div class="dum-divider" aria-hidden="true"></div>
<div class="dum-anchor">
<button type="button" class="dum-trigger" id="dumTrigger" aria-haspopup="true" aria-expanded="false" aria-controls="dumMenu">
<span class="dum-trigger__avatar" aria-hidden="true">椿</span>
<span class="dum-trigger__text">
<span class="dum-trigger__name">椿 このは</span>
<span class="dum-trigger__role">オーナー</span>
</span>
<span class="dum-trigger__arrow" aria-hidden="true">▴</span>
</button>
<ul class="dum-menu" id="dumMenu" role="menu" aria-labelledby="dumTrigger" hidden>
<li class="dum-menu__header" role="none">
<span class="dum-menu__avatar" aria-hidden="true">椿</span>
<span class="dum-menu__who">
<span class="dum-menu__name">椿 このは</span>
<span class="dum-menu__mail">konoha@example.com</span>
</span>
</li>
<li class="dum-sep" role="separator"></li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" data-action="profile">
<span class="dum-item__icon" aria-hidden="true">👤</span>
<span class="dum-item__label">プロフィール</span>
</button>
</li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" data-action="settings">
<span class="dum-item__icon" aria-hidden="true">⚙</span>
<span class="dum-item__label">設定</span>
</button>
</li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" id="dumThemeBtn">
<span class="dum-item__icon" aria-hidden="true" id="dumThemeIcon">☀</span>
<span class="dum-item__label">テーマ:<span id="dumThemeLabel">ライト</span></span>
</button>
</li>
<li class="dum-sep" role="separator"></li>
<li role="none">
<button type="button" class="dum-item" role="menuitem" data-action="logout">
<span class="dum-item__icon" aria-hidden="true">⏻</span>
<span class="dum-item__label">ログアウト</span>
</button>
</li>
</ul>
</div>
<p class="dum-hint" id="dumStatus" aria-live="polite"></p>
</div>
</div>
【CSS】
/* パープル×ピンクの淡いグラデーションを背景にした、アバター起点のアカウントメニュー */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background:
radial-gradient(circle at 15% 15%, rgba(168, 85, 247, .12) 0%, transparent 45%),
radial-gradient(circle at 85% 15%, rgba(236, 72, 153, .10) 0%, transparent 45%),
#f7f5fb;
color: #241f2e;
}
.dum-stage { width: 100%; max-width: 300px; }
.dum-card {
width: 100%;
padding: 18px 16px 16px;
background: #fff;
border: 1px solid #ece7f5;
border-radius: 18px;
box-shadow: 0 22px 46px -24px rgba(88, 28, 135, .28);
display: flex;
flex-direction: column;
gap: 4px;
}
.dum-card__label { margin: 0; font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #a78bc4; }
.dum-card__name { margin: 0 0 10px; font-size: 15px; font-weight: 700; color: #241f2e; }
.dum-divider { height: 1px; background: #efe9f7; margin: 0 0 12px; }
.dum-anchor { position: relative; }
.dum-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
font: inherit;
text-align: left;
background: none;
border: none;
border-radius: 12px;
cursor: pointer;
transition: background .16s ease;
}
.dum-trigger:hover { background: #f7f3fb; }
.dum-trigger:focus-visible { outline: 2px solid #a855f7; outline-offset: 2px; }
.dum-trigger[aria-expanded="true"] { background: #f1eafb; }
.dum-trigger__avatar {
flex: none;
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
color: #fff;
background: linear-gradient(145deg, #a855f7, #ec4899);
}
.dum-trigger__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dum-trigger__name { font-size: 13px; font-weight: 700; color: #241f2e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dum-trigger__role { font-size: 11px; color: #9c93ab; }
.dum-trigger__arrow { flex: none; font-size: 11px; color: #a78bc4; transition: transform .2s ease, color .2s ease; }
.dum-trigger[aria-expanded="true"] .dum-trigger__arrow { transform: translateY(-2px); color: #7c3aed; }
.dum-menu {
position: absolute;
z-index: 10;
left: 0;
right: 0;
bottom: calc(100% + 10px);
margin: 0;
padding: 8px;
list-style: none;
background: #fff;
border: 1px solid #ece7f5;
border-radius: 16px;
box-shadow: 0 26px 54px -18px rgba(88, 28, 135, .35);
transform-origin: bottom center;
opacity: 0;
transform: translateY(6px) scale(.97);
transition: opacity .16s ease, transform .16s ease;
}
.dum-menu[hidden] { display: none; }
.dum-menu.is-open { opacity: 1; transform: translateY(0) scale(1); }
.dum-menu__header { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; }
.dum-menu__avatar {
flex: none;
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 14px;
font-weight: 700;
color: #fff;
background: linear-gradient(145deg, #a855f7, #ec4899);
}
.dum-menu__who { display: flex; flex-direction: column; min-width: 0; }
.dum-menu__name { font-size: 13px; font-weight: 700; color: #241f2e; }
.dum-menu__mail { font-size: 11px; color: #9c93ab; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dum-sep { height: 1px; margin: 4px 4px; background: #efe9f7; }
.dum-item {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 9px 8px;
font: inherit;
font-size: 13px;
color: #362f42;
background: none;
border: none;
border-radius: 10px;
text-align: left;
cursor: pointer;
transition: background .13s ease;
}
.dum-item:hover,
.dum-item.is-active { background: #f7f3fb; }
.dum-item:focus-visible { outline: 2px solid #a855f7; outline-offset: -2px; }
.dum-item__icon { flex: none; width: 18px; text-align: center; font-size: 13px; display: inline-block; }
.dum-item__label { flex: 1; }
#dumThemeIcon.is-cycled { animation: dumSpin .35s ease; }
@keyframes dumSpin {
from { transform: rotate(-40deg) scale(.7); opacity: .3; }
to { transform: rotate(0) scale(1); opacity: 1; }
}
.dum-hint { margin: 10px 0 0; min-height: 1.4em; font-size: 11.5px; color: #9c93ab; text-align: center; }
@media (prefers-reduced-motion: reduce) {
.dum-menu, .dum-trigger__arrow, #dumThemeIcon { transition: none; }
#dumThemeIcon.is-cycled { animation: none; }
}
【JavaScript】
// ユーザーメニュー: アバターをクリックすると上向きにアカウントメニューが開く。テーマ項目はクリックのたびに循環する
(() => {
const stage = document.querySelector('.dum-stage');
const trigger = document.getElementById('dumTrigger');
const menu = document.getElementById('dumMenu');
const status = document.getElementById('dumStatus');
const themeBtn = document.getElementById('dumThemeBtn');
const themeIcon = document.getElementById('dumThemeIcon');
const themeLabel = document.getElementById('dumThemeLabel');
if (!stage || !trigger || !menu || !status) return;
const items = Array.from(menu.querySelectorAll('.dum-item'));
if (!items.length) return;
const isOpen = () => trigger.getAttribute('aria-expanded') === 'true';
const setActive = (index) => {
items.forEach((it, i) => it.classList.toggle('is-active', i === index));
};
const open = () => {
menu.hidden = false;
void menu.offsetHeight; // 強制リフローでトランジションを確実に発火させる
menu.classList.add('is-open');
trigger.setAttribute('aria-expanded', 'true');
setActive(0);
items[0].focus();
};
const close = (focusTrigger) => {
menu.hidden = true;
menu.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
setActive(-1);
if (focusTrigger) trigger.focus();
};
trigger.addEventListener('click', () => {
if (isOpen()) close(false); else open();
});
trigger.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
open();
}
});
const focusByIndex = (index) => {
const len = items.length;
const i = ((index % len) + len) % len;
setActive(i);
items[i].focus();
};
menu.addEventListener('keydown', (e) => {
const current = items.indexOf(document.activeElement);
switch (e.key) {
// メニューは上に開くため、下キーで先頭側へ・上キーで末尾側へと直感に合わせて動かす
case 'ArrowDown': e.preventDefault(); focusByIndex(current + 1); break;
case 'ArrowUp': e.preventDefault(); focusByIndex(current - 1); break;
case 'Home': e.preventDefault(); focusByIndex(0); break;
case 'End': e.preventDefault(); focusByIndex(items.length - 1); break;
case 'Escape': e.preventDefault(); close(true); break;
case 'Tab': close(false); break;
default: break;
}
});
const THEMES = [
{ label: 'ライト', icon: '☀' },
{ label: 'ダーク', icon: '☾' },
{ label: 'システム', icon: '◐' },
];
let themeIndex = 0;
const cycleTheme = () => {
themeIndex = (themeIndex + 1) % THEMES.length;
const t = THEMES[themeIndex];
if (themeLabel) themeLabel.textContent = t.label;
if (themeIcon) {
themeIcon.textContent = t.icon;
themeIcon.classList.remove('is-cycled');
void themeIcon.offsetWidth; // 強制リフローで連続クリックでもアニメを再発火させる
themeIcon.classList.add('is-cycled');
}
status.textContent = `テーマを${t.label}に変更しました`;
};
items.forEach((item, index) => {
item.addEventListener('click', () => {
if (item === themeBtn) {
cycleTheme(); // テーマは開いたまま連続で切り替えられるようにする
return;
}
const label = item.querySelector('.dum-item__label');
const text = label ? label.textContent.trim() : item.textContent.trim();
status.textContent = `「${text}」を実行しました`;
close(true);
});
item.addEventListener('mouseenter', () => setActive(index));
});
document.addEventListener('click', (e) => {
if (isOpen() && !stage.contains(e.target)) close(false);
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。