Menu
A vertical navigation menu with nested options and icons. Supports unlimited nesting levels with proper indentation and visual hierarchy
Basic Usage
The menu accepts an array of options that define its structure. Each option can use the title property for simple text labels, or label for custom rendered content. Menu items support:
- Items with icons that can be hidden when expanded (
hideIconWhenExpanded) - Items with count badges displayed next to the title
- Nested children with unlimited depth
- Disabled states
- Dividers for visual separation
- Custom rendered items using Vue's
hfunction
My Inbox
12
Assigned to Me
10
Unread
10
All
12
Recent
2
Starred
Followed by me
Internal Chat
Team Inbox
3
Disabled
Manual Actions
vue
<template>
<HLMenu :options="menuOptions" v-model:value="menuValue" :default-expanded-keys="['inbox', 'assigned']" />
</template>
<script setup lang="ts">
import { HLMenu } from '@platform-ui/highrise'
import { ref } from 'vue'
import { menuOptions } from './options'
const menuValue = ref('unread')
</script>ts
import { renderIcon } from '@platform-ui/highrise'
import {
User01Icon,
UserLeft01Icon,
UserUp01Icon,
Users01Icon,
Home01Icon,
BezierCurve03Icon,
} from '@gohighlevel/ghl-icons/24/outline'
export const menuOptions = [
{
key: 'inbox',
title: 'My Inbox',
icon: renderIcon(User01Icon),
hideIconWhenExpanded: true,
count: '12',
children: [
{
key: 'assigned',
title: 'Assigned to Me',
icon: renderIcon(UserLeft01Icon),
count: '10',
children: [
{ key: 'unread', title: 'Unread', count: '10' },
{ key: 'all', title: 'All', count: '12' },
{ key: 'recent', title: 'Recent', count: '2' },
{ key: 'starred', title: 'Starred' },
],
},
{ key: 'followed', title: 'Followed by me', icon: renderIcon(UserUp01Icon) },
{ key: 'chat', title: 'Internal Chat', icon: renderIcon(BezierCurve03Icon) },
],
},
{
key: 'team-inbox',
title: 'Team Inbox',
icon: renderIcon(Users01Icon),
hideIconWhenExpanded: true,
count: '3',
children: [
{ key: 'unread2', title: 'Unread', count: '10' },
{ key: 'all2', title: 'All', count: '12' },
{ key: 'recent2', title: 'Recent', count: '2' },
{ key: 'starred2', title: 'Starred' },
],
},
{ key: 'disabled', title: 'Disabled', icon: renderIcon(User01Icon), disabled: true },
{ key: 'divider-1', type: 'divider' },
{ key: 'manual', title: 'Manual Actions', icon: renderIcon(Home01Icon), hideIconWhenExpanded: true },
]Layout Example
Complete example showing how to use the menu in a typical sidebar layout with header, body, and footer sections.
Content Area
Active menu item: unread
Search value:
Menu collapsed: false
vue
<template>
<HLMenuLayout
:collapsed="layoutCollapsed"
:show-trigger="true"
:width="280"
:collapsed-width="52"
@update:collapsed="layoutCollapsed = $event"
>
<template #sider>
<div class="sider">
<!-- Header -->
<div class="sider-header">
<!-- Full-width button when expanded, icon-only when collapsed -->
<HLButton
v-if="!layoutCollapsed"
variant="primary"
color="blue"
size="xs"
style="width: 100%; margin-bottom: 10px"
>
<template #iconLeft>
<MessagePlusCircleIcon />
</template>
New Conversation
</HLButton>
<HLButton
v-else
variant="primary"
color="blue"
size="xs"
style="width: 100%; padding: 8px; margin-bottom: 8px"
@click="layoutCollapsed = false"
>
<template #iconLeft>
<MessagePlusCircleIcon />
</template>
</HLButton>
<!-- Searchable select when expanded -->
<HLSelect
v-if="!layoutCollapsed"
id="menu-search-select"
v-model:value="searchValue"
:filterable="true"
:options="selectOptions"
placeholder="Search or select..."
show-search-icon
style="width: 100%"
>
<template #arrow>
<FilterLinesIcon style="width: 16px; height: 16px; color: var(--gray-600)" />
</template>
</HLSelect>
<!-- Search affordance when collapsed -->
<div v-else class="search-box" title="Click to expand and search" @click="layoutCollapsed = false">
<SearchMdIcon style="width: 16px; height: 16px; color: var(--gray-600)" />
</div>
</div>
<!-- Body - main menu -->
<div class="sider-body">
<HLMenu
id="menu"
:options="bodyMenuOptions"
:value="activeValue"
:collapsed="layoutCollapsed"
:default-expanded-keys="['team-inbox', 'inbox', 'assigned']"
:submenu-width="200"
:indent="16"
:root-indent="8"
style="height: 100%"
@update:value="activeValue = $event"
/>
</div>
<!-- Footer menu -->
<HLMenu
id="footer-menu"
:options="footerMenuOptions"
:value="activeValue"
:collapsed="layoutCollapsed"
:indent="16"
:root-indent="8"
@update:value="activeValue = $event"
/>
</div>
</template>
<template #content>
<div style="padding: 16px">
<h3>Content Area</h3>
<p>Active menu item: {{ activeValue }}</p>
<p>Search value: {{ searchValue }}</p>
<p>Menu collapsed: {{ layoutCollapsed }}</p>
</div>
</template>
</HLMenuLayout>
</template>
<script setup lang="ts">
import { HLMenuLayout, HLMenu, HLButton, HLSelect } from '@platform-ui/highrise'
import {
MessagePlusCircleIcon,
SearchMdIcon,
FilterLinesIcon,
} from '@gohighlevel/ghl-icons/24/outline'
import { ref } from 'vue'
import { bodyMenuOptions, footerMenuOptions } from './options'
const layoutCollapsed = ref(false)
const searchValue = ref(null)
const activeValue = ref('unread')
const selectOptions = [
{ label: 'My Inbox', value: 'inbox' },
{ label: 'Team Inbox', value: 'team-inbox' },
{ label: 'Settings', value: 'settings' },
{ label: 'Manual Actions', value: 'manual' },
]
</script>
<style scoped>
.sider {
display: flex;
flex-direction: column;
height: 100%;
background-color: #eceef2;
}
.sider-header {
padding: 16px 10px 0;
border-bottom: 1px solid var(--gray-200);
}
.sider-body {
flex: 1;
overflow: hidden;
}
.search-box {
width: 100%;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: white;
border: 1px solid var(--gray-300);
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.search-box:hover {
background-color: var(--gray-50);
}
</style>ts
import { renderIcon } from '@platform-ui/highrise'
import {
User01Icon,
UserLeft01Icon,
UserUp01Icon,
Users01Icon,
Home01Icon,
BezierCurve03Icon,
Settings02Icon,
LogOut01Icon,
} from '@gohighlevel/ghl-icons/24/outline'
export const bodyMenuOptions = [
{
key: 'inbox',
title: 'My Inbox',
icon: renderIcon(User01Icon),
hideIconWhenExpanded: true,
count: '12',
children: [
{
key: 'assigned',
title: 'Assigned to Me',
icon: renderIcon(UserLeft01Icon),
count: '10',
children: [
{ key: 'unread', title: 'Unread', count: '10' },
{ key: 'all', title: 'All', count: '12' },
{ key: 'recent', title: 'Recent', count: '2' },
{ key: 'starred', title: 'Starred' },
],
},
{ key: 'followed', title: 'Followed by me', icon: renderIcon(UserUp01Icon) },
{ key: 'chat', title: 'Internal Chat', icon: renderIcon(BezierCurve03Icon) },
],
},
{
key: 'team-inbox',
title: 'Team Inbox',
icon: renderIcon(Users01Icon),
hideIconWhenExpanded: true,
count: '3',
children: [
{ key: 'unread2', title: 'Unread', count: '10' },
{ key: 'all2', title: 'All', count: '12' },
{ key: 'recent2', title: 'Recent', count: '2' },
{ key: 'starred2', title: 'Starred' },
],
},
{ key: 'disabled', title: 'Disabled', icon: renderIcon(User01Icon), disabled: true },
{ key: 'divider-1', type: 'divider' },
{ key: 'manual', title: 'Manual Actions', icon: renderIcon(Home01Icon), hideIconWhenExpanded: true },
]
export const footerMenuOptions = [
{ key: 'settings', title: 'Settings', icon: renderIcon(Settings02Icon), hideIconWhenExpanded: true },
{ key: 'logout', title: 'Log Out', icon: renderIcon(LogOut01Icon), hideIconWhenExpanded: true },
]Accessibility
- Give the menu container
role="navigation"(ormenu) plusaria-label/aria-labelledbyexplaining what it lists. - Mark the active route using
aria-current="page"/aria-selected, and togglearia-expandedon items that reveal submenus. - Route helper text or badge counts through
aria-describedbyso they are read with each item.
Imports
ts
import { HLMenu } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
| id | string | Auto (hr-menu-*) | The id of the element. Generated automatically when omitted. |
| options | MenuOption[] | [] | Items data of menu. |
| value | string | number | undefined | undefined | Selected key of the menu item. |
| defaultExpandedKeys | Array<string | number> | [] | Keys expanded on initial render for uncontrolled use; the menu then manages expansion itself. Ignored when expandedKeys is set. |
| expandedKeys | Array<string | number> | undefined | undefined | Controlled expanded key list. When provided it fully drives expansion (and defaultExpandedKeys is ignored) — keep it in sync via @update:expanded-keys. |
| collapsed | boolean | false | Whether the menu is in collapsed state (icon-only mode). |
| submenuWidth | number | 182 | Width of the dropdown submenu in collapsed mode (in pixels). |
| indent | number | 16 | Indentation for nested menu items (in pixels). |
| rootIndent | number | 8 | Indentation for root level menu items (in pixels). |
| ariaLabel | string | undefined | undefined | Accessible label for the menu container. |
| ariaLabelledby | string | undefined | undefined | Id of an element that labels the menu container. |
| ariaDescribedby | string | undefined | undefined | Id of an element that describes the menu container. |
| tooltipVariant | 'light' | 'dark' | 'dark' | Variant of the tooltip that shows an item's label when the menu is collapsed. |
In collapsed mode only icons are visible, so hovering an item reveals its label in a tooltip. Use tooltipVariant to switch that tooltip between dark (default) and light.
Types
MenuOption
ts
interface MenuOption {
key: string | number // Unique identifier
label?: string | (() => VNode) // Text or custom render function
title?: string // Convenience field for simple text labels (alternative to label)
icon?: () => VNode // Optional icon
count?: string | number // Optional count badge
hideIconWhenExpanded?: boolean // Hide icon when menu is expanded
disabled?: boolean // Disable the menu item
children?: MenuOption[] // Nested menu items
type?: 'group' | 'divider' // Special item types
}Emits
| Name | Type | Description |
|---|---|---|
update:value | (key: string, item: MenuOption) | Emitted when the value changes |
update:expanded-keys | (keys: string[]) | Emitted when the expanded keys change. Pair with the expandedKeys prop for controlled expansion. (@update:expanded-key, singular, is also emitted as an alias.) |
Methods
| Name | Type | Description |
|---|---|---|
deriveResponsiveState | () => void | Recalculates the collapsed state of the responsive menu content. Use this method when the menu container's width changes dynamically to ensure proper expansion behavior in responsive mode. |
showOption | (key: string| number) => void | Show the option |