Components Medium
Action Dropdown Menu
A three-dot floating menu with grouped action items, copy-to-clipboard, keyboard support, and snackbar feedback.
Open in Lab
MCP
css javascript
Targets: HTML
Code
:root {
color-scheme: dark;
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
background: #080808;
color: #e2e8f0;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
}
/* Trigger button */
.menu-wrapper {
position: relative;
display: inline-flex;
}
.menu-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.menu-trigger:hover,
.menu-trigger[aria-expanded="true"] {
background: rgba(255, 255, 255, 0.1);
color: #f8fafc;
border-color: rgba(255, 255, 255, 0.14);
}
/* Dropdown panel */
.action-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 50;
min-width: 200px;
padding: 6px;
border-radius: 10px;
background: #161616;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
opacity: 0;
transform: scale(0.95) translateY(-4px);
transform-origin: top right;
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
}
.action-menu.open {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
/* Groups */
.menu-group {
display: flex;
flex-direction: column;
gap: 1px;
}
.menu-group-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.28);
padding: 4px 10px 2px;
}
/* Items */
.menu-item {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
padding: 7px 10px;
border-radius: 6px;
background: none;
border: none;
color: rgba(255, 255, 255, 0.65);
font-size: 0.8125rem;
font-family: inherit;
cursor: pointer;
text-align: left;
transition: background 0.12s, color 0.12s;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.07);
color: #f8fafc;
}
.menu-item--danger:hover {
background: rgba(248, 113, 113, 0.1);
color: #f87171;
}
/* Divider */
.menu-divider {
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.06);
margin: 4px 0;
}
/* Snackbar */
.snackbar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(12px);
padding: 8px 18px;
border-radius: 8px;
background: #f8fafc;
color: #0a0a0a;
font-size: 0.8125rem;
font-weight: 500;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
white-space: nowrap;
z-index: 200;
}
.snackbar.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Action Dropdown Menu</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<div class="menu-wrapper">
<button
class="menu-trigger"
id="menu-trigger"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="action-menu"
aria-label="More actions"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="5" r="1.5"/>
<circle cx="12" cy="12" r="1.5"/>
<circle cx="12" cy="19" r="1.5"/>
</svg>
</button>
<div class="action-menu" id="action-menu" role="menu" aria-hidden="true">
<!-- Group 1: Copy actions -->
<div class="menu-group">
<button class="menu-item" role="menuitem" data-action="copy-snippet">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
Copy snippet
</button>
<button class="menu-item" role="menuitem" data-action="copy-markdown">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
Copy as Markdown
</button>
</div>
<hr class="menu-divider" />
<!-- Group 2: Open in AI tools -->
<div class="menu-group">
<p class="menu-group-label">Open in AI</p>
<button class="menu-item" role="menuitem" data-action="open-chatgpt">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494z"/>
</svg>
ChatGPT
</button>
<button class="menu-item" role="menuitem" data-action="open-claude">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M4.709 15.955l4.72-8.734.003-.006a.3.3 0 0 0-.053-.318L7.416 4.958a.3.3 0 0 0-.516.2l-.002 3.325-2.189-3.8a.3.3 0 0 0-.518.001l-3.118 5.4a.3.3 0 0 0 .26.45l3.376-.003zM20.8 8.22l-3.149-5.454a.3.3 0 0 0-.52.001l-2.174 3.795-.002-3.31a.3.3 0 0 0-.516-.202L12.47 4.9a.3.3 0 0 0-.052.316l4.716 8.73-.001.001 3.408.003a.3.3 0 0 0 .259-.45zm-8.138 8.433l-2.17-3.78 2.171-3.78h4.34l2.17 3.78-2.17 3.78zm-5.755.112l3.376.003 1.476 2.553a.3.3 0 0 0 .518.001l2.174-3.795.002 3.31a.3.3 0 0 0 .516.202l1.969-1.949a.3.3 0 0 0 .052-.316l-4.716-8.73.001-.001-3.408-.003a.3.3 0 0 0-.259.45zm-4.708 7.291l3.149 5.454a.3.3 0 0 0 .52-.001l2.174-3.795.002 3.31a.3.3 0 0 0 .516.202l1.97-1.949a.3.3 0 0 0 .052-.316l-4.716-8.73H3.24a.3.3 0 0 0-.26.45z"/>
</svg>
Claude
</button>
</div>
<hr class="menu-divider" />
<!-- Group 3: Export -->
<div class="menu-group">
<button class="menu-item" role="menuitem" data-action="open-codepen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 8.18L12 0 0 8.18v7.64L12 24l12-8.18V8.18zm-2 1.48v4.68l-3.34-2.34 3.34-2.34zM13 3.35l7.94 5.4-3.44 2.41L13 8.31V3.35zm-2 0v4.96L7.5 11.16 4.06 8.75 11 3.35zM2 9.66l3.34 2.34L2 14.34V9.66zm9 11-7.94-5.4 3.44-2.41L11 15.7v4.96zm1-6.61-2.9-2.05L12 9.95l2.9 2.05L12 14.05zm1 6.61V15.7l4.5-3.16 3.44 2.41L13 20.66z"/>
</svg>
Open in CodePen
</button>
</div>
</div>
</div>
</main>
<!-- Snackbar -->
<div class="snackbar" id="snackbar" role="status" aria-live="polite"></div>
<script>
const trigger = document.getElementById('menu-trigger');
const menu = document.getElementById('action-menu');
const snackbar = document.getElementById('snackbar');
let snackTimer = null;
function openMenu() {
menu.classList.add('open');
menu.setAttribute('aria-hidden', 'false');
trigger.setAttribute('aria-expanded', 'true');
}
function closeMenu() {
menu.classList.remove('open');
menu.setAttribute('aria-hidden', 'true');
trigger.setAttribute('aria-expanded', 'false');
}
function showSnack(msg) {
snackbar.textContent = msg;
snackbar.classList.add('show');
clearTimeout(snackTimer);
snackTimer = setTimeout(() => snackbar.classList.remove('show'), 2200);
}
trigger.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.contains('open') ? closeMenu() : openMenu();
});
menu.addEventListener('click', (e) => e.stopPropagation());
document.addEventListener('click', closeMenu);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
menu.addEventListener('click', (e) => {
const item = e.target.closest('[data-action]');
if (!item) return;
switch (item.dataset.action) {
case 'copy-snippet':
navigator.clipboard.writeText('// Your snippet code here').then(() => showSnack('Snippet copied!'));
break;
case 'copy-markdown':
navigator.clipboard.writeText('```js\n// Your snippet code here\n```').then(() => showSnack('Markdown copied!'));
break;
case 'open-chatgpt':
window.open('https://chatgpt.com', '_blank', 'noopener');
break;
case 'open-claude':
window.open('https://claude.ai', '_blank', 'noopener');
break;
case 'open-codepen':
showSnack('Opening in CodePen…');
break;
}
closeMenu();
});
</script>
</body>
</html>Action Dropdown Menu
A floating context menu triggered by a three-dot (kebab) button. Groups related actions with dividers, handles clipboard writes, and shows a snackbar notification on completion.
Features
- Click-outside detection closes the menu
- Escape key support
- Action groups separated by
<hr>dividers - Copy-to-clipboard with snackbar feedback
- Fully keyboard accessible (ARIA
haspopup,expanded)
When to use
- Resource or card overflow menus
- Table row action columns
- Any contextual action panel attached to a trigger button