ポップオーバーメニュー
ホバーではなく、クリック(またはフォーカス)で開き、アクションリストを表示するポップオーバーメニュー。
ポップオーバーメニューは、ホバーで表示されるツールチップとは異なり、クリックで開き、アクション(編集や削除など)のリストをユーザーに提示するために使用されます。フォーカス管理やクリック外での閉じる処理など、アクセシビリティも考慮した設計です。
プレビュー (Preview)
ユースケースと特徴 (Use Cases & Features)
-
オプションの提示: メニューアイコン(…)やボタンをクリックして、各種アクション(編集、削除、共有など)を表示するUIに最適です。
-
アクセシビリティ考慮:
aria-expandedやaria-haspopup属性を使用し、Escapeキーでの非表示やフォーカス管理をサポートしています。 -
滑らかなアニメーション: 不自然な表示切り替えを防ぐため、Y軸方向へのスライドとフェードインを組み合わせたトランジションを実装しています。
デザインとUXのポイント (Design & UX)
-
明瞭な階層: 影(
box-shadow)を使って浮遊感を表現し、背景コンテンツとの分離を明確にしています。 -
安全な操作: クリック範囲外の領域を押すことでメニューが閉じる(Click Outside)処理が含まれており、モバイルでもPCでも直感的に操作できます。
-
破壊的アクションの強調: 削除など元に戻せないアクションには赤いテキスト(Danger)を使用し、ユーザーの誤操作を減らすデザインを取り入れています。
カスタマイズ方法 (Customization Guide)
-
メニューの幅を変更する場合は
.popover-menuのwidthを調整してください。 -
開く方向を右下や左下にする場合は、
.popover-menuのtransformやleftプロパティを変更することで容易にカスタマイズ可能です。
実装コード (Implementation)
以下のコードを使用して、プロジェクトにポップオーバーメニューを組み込むことができます。
<div class="popover-wrapper">
<button class="popover-trigger" aria-haspopup="true" aria-expanded="false" id="popover-btn">
<span>アクション</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="popover-menu" role="menu" aria-labelledby="popover-btn">
<div class="popover-content">
<button class="popover-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
編集する
</button>
<button class="popover-item" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M21 9H3"/><path d="M9 21V9"/></svg>
複製する
</button>
<div class="popover-divider"></div>
<button class="popover-item text-danger" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
削除する
</button>
</div>
</div>
</div>
<style>
/* CSSをここに配置 */
.popover-wrapper {
position: relative;
display: inline-block;
}
.popover-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: #334155;
background-color: #ffffff;
border: 1px solid #cbd5e1;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.popover-trigger:hover {
background-color: #f1f5f9;
border-color: #94a3b8;
}
.popover-trigger:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.popover-trigger svg {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.popover-trigger.is-open svg {
transform: rotate(180deg);
}
.popover-menu {
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(-10px);
width: 200px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
opacity: 0;
visibility: hidden;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 50;
pointer-events: none;
}
.popover-wrapper.is-active .popover-menu {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
pointer-events: auto;
}
.popover-content {
padding: 6px;
display: flex;
flex-direction: column;
gap: 2px;
}
.popover-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 12px;
font-size: 14px;
color: #475569;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
text-align: left;
transition: background-color 0.15s ease, color 0.15s ease;
}
.popover-item:hover,
.popover-item:focus-visible {
background-color: #f1f5f9;
color: #0f172a;
outline: none;
}
.popover-item.text-danger {
color: #ef4444;
}
.popover-item.text-danger:hover,
.popover-item.text-danger:focus-visible {
background-color: #fef2f2;
color: #dc2626;
}
.popover-divider {
height: 1px;
background-color: #e2e8f0;
margin: 4px 0;
}
</style>
<script>
// JavaScriptをここに配置
document.addEventListener('DOMContentLoaded', () => {
const wrappers = document.querySelectorAll('.popover-wrapper');
wrappers.forEach(wrapper => {
const trigger = wrapper.querySelector('.popover-trigger');
const menu = wrapper.querySelector('.popover-menu');
if (!trigger || !menu) return;
const togglePopover = () => {
const isActive = wrapper.classList.contains('is-active');
if (isActive) {
closePopover();
} else {
openPopover();
}
};
const openPopover = () => {
wrapper.classList.add('is-active');
trigger.classList.add('is-open');
trigger.setAttribute('aria-expanded', 'true');
};
const closePopover = () => {
wrapper.classList.remove('is-active');
trigger.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
};
trigger.addEventListener('click', togglePopover);
// クリック領域外で閉じる
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
closePopover();
}
});
// Escapeキーで閉じる
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && wrapper.classList.contains('is-active')) {
closePopover();
trigger.focus();
}
});
});
});
</script>