Skip to content

Menu

Build context menus, action lists, and navigation panels with full keyboard support. Menus group related actions together, making them easy to discover and interact with. They support nested submenus, icons, section headings, and danger variants for destructive actions.

ts
import { CoarMenu, CoarMenuItem, CoarMenuDivider, CoarMenuHeading, CoarSubExpand, CoarSubFlyout } from '@cocoar/vue-ui';

Basic Menu

The simplest menu: a list of clickable items separated by dividers. Individual items can be disabled when an action is not available.

Last clicked: none

vue
<template>
  <div>
    <CoarMenu>
      <CoarMenuItem @clicked="handleClick('New File')">New File</CoarMenuItem>
      <CoarMenuItem @clicked="handleClick('Open...')">Open...</CoarMenuItem>
      <CoarMenuDivider />
      <CoarMenuItem @clicked="handleClick('Save')">Save</CoarMenuItem>
      <CoarMenuItem @clicked="handleClick('Save As...')">Save As...</CoarMenuItem>
      <CoarMenuDivider />
      <CoarMenuItem :disabled="true">Export (disabled)</CoarMenuItem>
    </CoarMenu>
    <p style="margin-top: 8px; font-size: 13px; color: var(--coar-text-neutral-secondary);">Last clicked: {{ lastClicked || 'none' }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { CoarMenu, CoarMenuItem, CoarMenuDivider } from '@cocoar/vue-ui';

const lastClicked = ref('');

function handleClick(item: string) {
  lastClicked.value = item;
}
</script>

With Headings

Use CoarMenuHeading to organize a longer menu into labeled sections. This helps users scan for the action they need without reading every item.

vue
<template>
  <CoarMenu>
    <CoarMenuHeading>File</CoarMenuHeading>
    <CoarMenuItem>New</CoarMenuItem>
    <CoarMenuItem>Open</CoarMenuItem>
    <CoarMenuItem>Recent</CoarMenuItem>
    <CoarMenuDivider />
    <CoarMenuHeading>Edit</CoarMenuHeading>
    <CoarMenuItem>Cut</CoarMenuItem>
    <CoarMenuItem>Copy</CoarMenuItem>
    <CoarMenuItem>Paste</CoarMenuItem>
  </CoarMenu>
</template>

<script setup lang="ts">
import { CoarMenu, CoarMenuItem, CoarMenuDivider, CoarMenuHeading } from '@cocoar/vue-ui';
</script>

With Icons

Leading icons give each item a visual anchor, making menus faster to scan. Use a trash icon on destructive actions like "Delete" to signal their intent.

vue
<template>
  <CoarMenu>
    <CoarMenuItem icon="plus">New File</CoarMenuItem>
    <CoarMenuItem icon="copy">Duplicate</CoarMenuItem>
    <CoarMenuItem icon="clipboard">Paste</CoarMenuItem>
    <CoarMenuDivider />
    <CoarMenuItem icon="settings">Settings</CoarMenuItem>
    <CoarMenuItem icon="trash-2">Delete</CoarMenuItem>
  </CoarMenu>
</template>

<script setup lang="ts">
import { CoarMenu, CoarMenuItem, CoarMenuDivider } from '@cocoar/vue-ui';
</script>

Nested Submenus

When a menu item leads to a group of related options, wrap them in CoarSubExpand. Submenus expand inline, keeping the user in context without opening a separate overlay.

vue
<template>
  <CoarMenu>
    <CoarMenuItem @clicked="handleClick('Dashboard')">Dashboard</CoarMenuItem>
    <CoarSubExpand label="Settings">
      <CoarMenuItem @clicked="handleClick('Profile')">Profile</CoarMenuItem>
      <CoarMenuItem @clicked="handleClick('Security')">Security</CoarMenuItem>
      <CoarMenuItem @clicked="handleClick('Notifications')">Notifications</CoarMenuItem>
    </CoarSubExpand>
    <CoarSubExpand label="Reports">
      <CoarMenuItem @clicked="handleClick('Sales')">Sales</CoarMenuItem>
      <CoarMenuItem @clicked="handleClick('Traffic')">Traffic</CoarMenuItem>
    </CoarSubExpand>
    <CoarMenuDivider />
    <CoarMenuItem @clicked="handleClick('Logout')">Logout</CoarMenuItem>
  </CoarMenu>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { CoarMenu, CoarMenuItem, CoarMenuDivider, CoarSubExpand } from '@cocoar/vue-ui';

const lastClicked = ref('');

function handleClick(item: string) {
  lastClicked.value = item;
}
</script>

Flyout Submenus

Use CoarSubFlyout when the submenu should appear as a floating panel beside the trigger instead of expanding inline. The label prop sets the visible text; child items go in the default slot and render inside the flyout.

vue
<template>
  <CoarMenu>
    <CoarMenuItem icon="home" @clicked="handleClick('Home')">Home</CoarMenuItem>
    <CoarSubFlyout label="Account" icon="user">
      <CoarMenu>
        <CoarMenuItem icon="user" @clicked="handleClick('Profile')">Profile</CoarMenuItem>
        <CoarMenuItem icon="shield" @clicked="handleClick('Security')">Security</CoarMenuItem>
        <CoarMenuItem icon="bell" @clicked="handleClick('Notifications')">Notifications</CoarMenuItem>
      </CoarMenu>
    </CoarSubFlyout>
    <CoarSubFlyout label="Reports" icon="chart-bar">
      <CoarMenu>
        <CoarMenuItem @clicked="handleClick('Sales')">Sales</CoarMenuItem>
        <CoarMenuItem @clicked="handleClick('Traffic')">Traffic</CoarMenuItem>
      </CoarMenu>
    </CoarSubFlyout>
    <CoarMenuDivider />
    <CoarMenuItem icon="log-out" @clicked="handleClick('Logout')">Logout</CoarMenuItem>
  </CoarMenu>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { CoarMenu, CoarMenuItem, CoarMenuDivider, CoarSubFlyout } from '@cocoar/vue-ui';

const lastClicked = ref('');

function handleClick(item: string) {
  lastClicked.value = item;
}
</script>

Borderless (Sidebar)

Pass the borderless prop when embedding a menu inside a sidebar, panel, or card. This removes the outer border and background so the menu blends into its container.

vue
<template>
  <div style="background: var(--coar-background-neutral-secondary); border-radius: 8px; padding: 8px;">
    <CoarMenu borderless>
      <CoarMenuHeading>Navigation</CoarMenuHeading>
      <CoarMenuItem icon="home">Home</CoarMenuItem>
      <CoarMenuItem icon="user">Profile</CoarMenuItem>
      <CoarMenuItem icon="settings">Settings</CoarMenuItem>
    </CoarMenu>
  </div>
</template>

<script setup lang="ts">
import { CoarMenu, CoarMenuItem, CoarMenuHeading } from '@cocoar/vue-ui';
</script>

Scrollable Menu

When a menu has many items, it scrolls automatically. Use #header and #footer slots for fixed content above and below the scrollable area. CoarMenuHeading supports a sticky prop to keep section headers visible while scrolling.

vue
<template>
  <div style="display: flex; flex-direction: column; gap: 12px;">
    <CoarCheckbox v-model="sticky" label="sticky headings" />
  <div style="height: 280px; width: 240px;">
    <CoarMenu style="height: 100%; width: 100%;">
      <template #header>
        <div style="padding: 8px;">
          <input
            v-model="filter"
            type="text"
            placeholder="Filter..."
            style="width: 100%; box-sizing: border-box; padding: 4px 8px; border: 1px solid var(--coar-border-input); border-radius: var(--coar-radius-xs); background: var(--coar-surface-input); font-size: 13px; outline: none;"
          />
        </div>
      </template>

      <template v-for="item in filteredItems" :key="item.label">
        <CoarMenuDivider v-if="item.divider" />
        <CoarMenuHeading v-else-if="item.heading" :label="item.label" :sticky="sticky" />
        <CoarMenuItem v-else :icon="item.icon" :label="item.label" />
      </template>

      <template #footer>
        <CoarMenuItem icon="plus" label="New project..." />
      </template>
    </CoarMenu>
  </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { CoarMenu, CoarMenuItem, CoarMenuDivider, CoarMenuHeading, CoarCheckbox } from '@cocoar/vue-ui';

const filter = ref('');
const sticky = ref(false);

const items = [
  { heading: true, label: 'Navigation' },
  { icon: 'home', label: 'Dashboard' },
  { icon: 'users', label: 'Users' },
  { icon: 'settings', label: 'Settings' },
  { icon: 'bell', label: 'Notifications' },
  { divider: true, label: 'd1' },
  { heading: true, label: 'Projects' },
  { icon: 'folder', label: 'Frontend' },
  { icon: 'folder', label: 'Backend' },
  { icon: 'folder', label: 'Mobile App' },
  { icon: 'folder', label: 'Design System' },
  { icon: 'folder', label: 'Documentation' },
  { divider: true, label: 'd2' },
  { heading: true, label: 'Admin' },
  { icon: 'database', label: 'Database' },
  { icon: 'code', label: 'API Keys' },
  { icon: 'clipboard', label: 'Audit Log' },
  { icon: 'download', label: 'Exports' },
];

const filteredItems = computed(() => {
  const q = filter.value.toLowerCase().trim();
  if (!q) return items;
  return items.filter((item) =>
    item.heading || item.divider || item.label.toLowerCase().includes(q),
  );
});
</script>

Accessibility

Keyboard Navigation

KeyAction
Arrow Up / Arrow DownMove focus between items
Enter / SpaceActivate focused item
EscapeClose nested submenus
TabMove focus out of the menu

API

CoarMenu Props

PropTypeDefaultDescription
showIconColumnbooleantrueReserve icon column to prevent layout shift
borderlessbooleanfalseRemove outer border/background

CoarMenu Slots

SlotDescription
defaultMenu items (scrollable area)
headerFixed content above the scrollable area
footerFixed content below the scrollable area

CoarMenuHeading Props

PropTypeDefaultDescription
labelstringundefinedHeading text (alternative to default slot)
stickybooleanfalseStick to top of scroll container

CoarMenuItem Props

PropTypeDefaultDescription
labelstringundefinedItem label text (alternative to default slot)
iconstringundefinedLeading icon name
disabledbooleanfalseDisable the item

CoarMenuItem Slots

SlotDescription
defaultItem label content

CoarMenuItem Events

EventPayloadDescription
clickedMenuItemClickEventEmitted when item is clicked
ts
interface MenuItemClickEvent {
  event: MouseEvent;
  keepMenuOpen(): void; // Call to prevent auto-close of the menu tree
}

Released under the Apache-2.0 License.