Components · Navigation
Dropdown
A trigger button that reveals a floating overlay of actions — with leading icons, dividers, disabled rows and a red danger action.
Examples
Placement
Content
Dropdown · icons · a divider · a disabled row · a red danger action · a submenu → adjacent flyout — click to reveal, hover / ArrowRight to open a submenu
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-dropdown> is the SAME shared custom element React and Vue consume — here in its
native form. Theming comes only from the --aha-* tokens in tokens.css. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/icons.js'; // registers <aha-icon> for leading + caret glyphs
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-dropdown.js'; // registers <aha-dropdown>
</script>
<!-- placement="bottomLeft" (default) | "bottomRight" | "topLeft" | "topRight"
trigger="click" (default) | "hover"
items: leaf {key,label,icon?,disabled?,danger?} · submenu-parent {key,label,icon?,children:[…]}
(opens an adjacent flyout — hover / ArrowRight / Enter; ArrowLeft / Escape closes it)
· {type:"divider"} · {type:"group",label,children:[]} -->
<aha-dropdown id="menu" label="Actions" placement="bottomLeft"
items='[
{"key":"rename","label":"Rename","icon":"system-pencil-simple"},
{"key":"export","label":"Export","icon":"system-export","children":[
{"key":"pdf","label":"PDF","icon":"system-file"},
{"key":"pptx","label":"PowerPoint","icon":"system-file"},
{"key":"xlsx","label":"Excel","icon":"system-file"}
]},
{"type":"divider"},
{"key":"delete","label":"Delete","icon":"system-trash","danger":true}
]'>
</aha-dropdown>
<script>
// Opens on click, closes on outside-click / Escape (focus returns to the trigger); choosing an
// item emits a composed `select`.
document.getElementById('menu').addEventListener('select', (e) => console.log('action', e.detail.key));
</script>
import '@ahaslides-product/design/icons'; // registers <aha-icon> for leading + caret glyphs
import '@ahaslides-product/design/aha-dropdown'; // registers <aha-dropdown>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper for the composed `select` event; React 19 can bind onSelect directly.
function AhaDropdown({ label, items, placement, trigger, onSelect }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
const h = (e) => onSelect?.(e.detail.key);
el.addEventListener('select', h);
return () => el.removeEventListener('select', h);
});
return <aha-dropdown ref={ref} label={label} placement={placement} trigger={trigger} items={JSON.stringify(items)} />;
}
// usage — leading icons, a submenu-parent (children[] → adjacent flyout), a divider, a red danger action
// placement: bottomLeft | bottomRight | topLeft | topRight · trigger: click | hover
// a submenu opens on hover / ArrowRight / Enter and closes on ArrowLeft / Escape (edge-collision flip)
<AhaDropdown
label="Actions"
placement="bottomLeft"
items={[
{ key: 'rename', label: 'Rename', icon: 'system-pencil-simple' },
{ key: 'export', label: 'Export', icon: 'system-export', children: [
{ key: 'pdf', label: 'PDF', icon: 'system-file' },
{ key: 'pptx', label: 'PowerPoint', icon: 'system-file' },
{ key: 'xlsx', label: 'Excel', icon: 'system-file' },
] },
{ type: 'divider' },
{ key: 'delete', label: 'Delete', icon: 'system-trash', danger: true },
]}
onSelect={(key) => runAction(key)}
/>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/icons'; // registers <aha-icon> for leading + caret glyphs
import '@ahaslides-product/design/aha-dropdown';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue — leading icons, a submenu-parent (children[] → adjacent flyout), a divider, a danger action
// placement: bottomLeft | bottomRight | topLeft | topRight · trigger: click | hover
// a submenu opens on hover / ArrowRight / Enter and closes on ArrowLeft / Escape (edge-collision flip)
<script setup>
import { computed } from 'vue';
const items = computed(() => JSON.stringify([
{ key: 'rename', label: 'Rename', icon: 'system-pencil-simple' },
{ key: 'export', label: 'Export', icon: 'system-export', children: [
{ key: 'pdf', label: 'PDF', icon: 'system-file' },
{ key: 'pptx', label: 'PowerPoint', icon: 'system-file' },
{ key: 'xlsx', label: 'Excel', icon: 'system-file' },
] },
{ type: 'divider' },
{ key: 'delete', label: 'Delete', icon: 'system-trash', danger: true },
]));
function onSelect(e) { runAction(e.detail.key); }
</script>
<template>
<aha-dropdown label="Actions" placement="bottomLeft" :items="items" @select="onSelect" />
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | Menu | The trigger button text |
items | JSON list | [] | Rows. Leaf {key,label,icon?,disabled?,danger?}; submenu-parent {key,label,icon?,children:[…]} → adjacent flyout; rule {type:'divider'}; section {type:'group',label,children:[]} |
placement | 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight' | bottomLeft | Where the overlay opens relative to the trigger |
trigger | 'click' | 'hover' | click | How the overlay is revealed |
select | CustomEvent | — | Composed event on choosing an item, detail { key } |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-dropdown'; // registers <aha-dropdown>
Agent feed for this component (absolute, fetchable anywhere): dropdown.agent.json · dropdown.md · dropdown.llms.txt
When to use
When to use
- Dropdown — a set of actions or options should sit behind a trigger to save space
- Menu — the same list belongs inline, always visible — not behind a trigger
- Select — the control picks a single value for a form field, not fires an action
A dropdown holds actions or navigation, not form values — use Select for a field. Keep the item list short; the panel closes on outside-click and Escape. Give each action a leading icon, rule off a destructive action with a divider and mark it danger. Reach for a submenu-parent (children[]) only when a group of related actions would otherwise crowd the top level — one level of flyout is plenty; deeper nesting is a menu smell. The overlay reuses the shared Menu item vocabulary rather than reinventing the row styling.
Surfaces
editor dashboard admin settings
Spec
Trigger height 40 · radius 8 · white bg, 1px border #E3E3E3, SemiBold 600 · Trigger hover border-hover #D3B4FF (motion-fast + ease-out) · Caret <aha-icon name="system-caret-down" size=16>, rotates 180° when open (motion-mid) · Panel elevated white, radius 8, 6 padding, min-width 200, soft shadow · Placement bottomLeft · bottomRight · topLeft · topRight — anchors the persistent panel · Trigger mode click (default) or hover · Open motion opacity + translateY on a persistent node (motion-mid + ease-out) · Item height 36 · radius 8 · 10px gap to a 16px leading icon · hover bg-hover #F7F7F7 · Danger item color-error #F5222D text, error-tinted hover · Divider 1px split #F1F1F1 rule, role=separator · Group title 11px uppercase, text-tertiary #8A8A8A, non-interactive section header · Disabled item text-disabled #B5B5B5, not-allowed · Submenu parent item with children[]; aria-haspopup=menu, trailing <aha-icon name="system-caret-right" size=14>; brand-tinted (bg-accent #F9F5FF, color-primary #6A1EBB) while its flyout is open · Flyout second elevated panel adjacent to the parent (left:100%), opens on hover / ArrowRight / Enter; fades+scales on a persistent node (motion-mid + ease-out) · Edge-collision flip flyout measures on open; if it overflows the viewport it flips to open on the left (right:100%) · Flyout keyboard Up/Down within the flyout, ArrowLeft / Escape closes it back to the parent (focus returns to the parent row)