Components · Navigation
Menu
A list of selectable options — a side nav, a settings list, an overflow menu — with icons, groups, submenus and danger actions.
Examples
Mode
Content
Menu · icons · a group · a submenu · a disabled row · a danger action
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-menu> 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-menu.js'; // registers <aha-menu>
</script>
<!-- mode="vertical" (default) | "inline" (sidebar tree) | "horizontal" (top nav bar) -->
<aha-menu id="nav" value="polls"
items='[
{"type":"group","label":"Workspace","children":[
{"key":"polls","label":"Polls","icon":"system-chart-bar"},
{"key":"quizzes","label":"Quizzes","icon":"system-question"}
]},
{"key":"library","label":"Content library","icon":"system-folder","children":[
{"key":"templates","label":"Templates"},
{"key":"archived","label":"Archived","disabled":true}
]},
{"type":"divider"},
{"key":"delete","label":"Delete workspace","icon":"system-trash","danger":true}
]'>
</aha-menu>
<script>
// Selection animates on a persistent row; each pick emits a composed `select` event.
document.getElementById('nav').addEventListener('select', (e) => console.log('selected', e.detail.key));
</script>
import '@ahaslides-product/design/icons'; // registers <aha-icon> for leading + caret glyphs
import '@ahaslides-product/design/aha-menu'; // registers <aha-menu>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper for the composed `select` event; React 19 can bind onSelect directly.
function AhaMenu({ items, value, mode, 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-menu ref={ref} value={value} mode={mode} items={JSON.stringify(items)} />;
}
// usage — icons, a titled group, a submenu, a danger action; mode: vertical | inline | horizontal
<AhaMenu
value={view}
mode="vertical"
items={[
{ type: 'group', label: 'Workspace', children: [
{ key: 'polls', label: 'Polls', icon: 'system-chart-bar' },
{ key: 'quizzes', label: 'Quizzes', icon: 'system-question' },
] },
{ key: 'library', label: 'Content library', icon: 'system-folder', children: [
{ key: 'templates', label: 'Templates' },
{ key: 'archived', label: 'Archived', disabled: true },
] },
{ type: 'divider' },
{ key: 'delete', label: 'Delete workspace', icon: 'system-trash', danger: true },
]}
onSelect={setView}
/>
// 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-menu';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue — icons, a titled group, a submenu, a danger action; mode: vertical | inline | horizontal
<script setup>
import { ref, computed } from 'vue';
const view = ref('polls');
const items = computed(() => JSON.stringify([
{ type: 'group', label: 'Workspace', children: [
{ key: 'polls', label: 'Polls', icon: 'system-chart-bar' },
{ key: 'quizzes', label: 'Quizzes', icon: 'system-question' },
] },
{ key: 'library', label: 'Content library', icon: 'system-folder', children: [
{ key: 'templates', label: 'Templates' },
{ key: 'archived', label: 'Archived', disabled: true },
] },
{ type: 'divider' },
{ key: 'delete', label: 'Delete workspace', icon: 'system-trash', danger: true },
]));
function onSelect(e) { view.value = e.detail.key; }
</script>
<template>
<aha-menu :value="view" mode="vertical" :items="items" @select="onSelect" />
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
items | JSON tree | [] | Rows. Leaf {key,label,icon?,disabled?,danger?}; submenu {key,label,icon?,children:[]}; section {type:'group',label,children:[]}; rule {type:'divider'} |
value | string | — | The selected item's key (brand-tinted row) |
mode | 'vertical' | 'inline' | 'horizontal' | vertical | Layout: compact list · indented sidebar tree · top nav bar |
select | CustomEvent | — | Composed event on selection, 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-menu'; // registers <aha-menu>
Agent feed for this component (absolute, fetchable anywhere): menu.agent.json · menu.md · menu.llms.txt
When to use
When to use
- Menu — a list of destinations or options — a side nav, a settings list, an overflow list
- Tabs — switching horizontal views within one screen
- Dropdown — the same list should sit inside a triggered overlay, not inline
One selection at a time — reserve the brand tint for the active row. Keep labels short and in sentence case. Group related rows under a title; rule off a destructive action with a divider and mark it danger. For a triggered overlay, put a Menu inside a Dropdown rather than reinventing the list.
Surfaces
editor dashboard settings admin
Spec
Container white surface, 1px border #E3E3E3, radius 8, 6 padding, min-width 200 · Row height 36 · radius 8 · SemiBold 600 · 10px gap to a 16px leading icon · Group title 11px uppercase, text-tertiary #8A8A8A, non-interactive section header · Divider 1px split #F1F1F1 rule, role=separator · Submenu caret rotates 90° + children expand inline (motion-mid), aria-expanded synced · Hover bg-hover #F7F7F7 (motion-fast + ease-out, persistent node) · Selected bg-accent #F9F5FF fill + color-primary #6A1EBB text · Danger color-error #F5222D text, error-tinted hover · Disabled text-disabled #B5B5B5, not-allowed