Skip to content

CMenu ​

A floating content container that positions itself relative to an activator element. Used as the foundation for dropdowns, selects, tooltips and other overlay components.

Basic usage ​

Show code
vue
<template>
  <c-menu
    width="auto"
    open-on-click
    close-on-click-outside
    close-on-content-click
    align="bottom"
    :offset-y="4"
  >
    <template #activator="{ on, activator }">
      <c-btn class="bg-indigo" v-bind="activator" v-on="on" style="gap:8px">
        <c-icon name="fas:folder" source="fa" :size="14" />
        File
        <c-icon name="fas:chevron-down" source="fa" :size="10" />
      </c-btn>
    </template>

    <c-card class="elevation-4" style="min-width:220px">
      <c-card-body class="py-1 px-0">
        <c-list>
          <c-list-item class="px-4" style="gap:12px" @click="notify('New file')">
            <c-icon name="fas:plus" source="fa" :size="13" style="width:14px;opacity:.55" />
            <span style="flex:1">New file</span>
            <span class="kb">⌘N</span>
          </c-list-item>
          <c-list-item class="px-4" style="gap:12px" @click="notify('Open')">
            <c-icon name="fas:folder" source="fa" :size="13" style="width:14px;opacity:.55" />
            <span style="flex:1">Open…</span>
            <span class="kb">⌘O</span>
          </c-list-item>
          <c-list-item class="px-4" style="gap:12px" @click="notify('Save')">
            <c-icon name="fas:save" source="fa" :size="13" style="width:14px;opacity:.55" />
            <span style="flex:1">Save</span>
            <span class="kb">⌘S</span>
          </c-list-item>
        </c-list>
        <div class="sep" />
        <c-list>
          <c-list-item class="px-4" style="gap:12px;color:#f44336" @click="notify('Delete')">
            <c-icon name="fas:trash" source="fa" :size="13" style="width:14px" />
            Delete file
          </c-list-item>
        </c-list>
      </c-card-body>
    </c-card>
  </c-menu>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const msg = ref('')
function notify(text: string) {
  msg.value = text
}
</script>

<style scoped>
.kb {
  font-size: 11px;
  color: var(--c-sys-color-on-surface-variant);
}
.sep {
  height: 1px;
  background: var(--c-sys-color-outline-variant);
  margin: 4px 0;
}
</style>

Hover ​

Use open-on-hover + close-on-leave for hover-triggered menus. open-delay / close-delay prevent accidental triggers.

Show code
vue
<template>
  <div class="d-flex align-center gap-1 pa-2 radius-8 elevation-1">
    <c-menu
      v-for="item in nav"
      :key="item.label"
      width="auto"
      open-on-hover
      close-on-leave
      align="bottom"
      :offset-y="2"
      :open-delay="80"
      :close-delay="140"
    >
      <template #activator="{ on, activator }">
        <c-btn variant="text" :class="item.color" v-bind="activator" v-on="on" style="gap:6px">
          <c-icon :name="item.icon" source="fa" :size="13" />
          {{ item.label }}
        </c-btn>
      </template>

      <c-card class="elevation-4" style="min-width:180px">
        <c-card-body class="py-1 px-0">
          <c-list>
            <c-list-item v-for="link in item.links" :key="link.label" class="px-4" style="gap:10px">
              <c-icon :name="link.icon" source="fa" :size="12" style="width:14px;opacity:.5" />
              {{ link.label }}
            </c-list-item>
          </c-list>
        </c-card-body>
      </c-card>
    </c-menu>
  </div>
</template>

<script setup lang="ts">
const nav = [
  {
    label: 'Products',
    icon: 'fas:briefcase',
    color: 'text-indigo',
    links: [
      { icon: 'fas:list-ul', label: 'UI Components' },
      { icon: 'fas:image', label: 'Icons' },
      { icon: 'fas:star', label: 'Themes' },
    ],
  },
  {
    label: 'Docs',
    icon: 'fas:code',
    color: 'text-light-blue',
    links: [
      { icon: 'fas:home', label: 'Getting started' },
      { icon: 'fas:file', label: 'Migration guide' },
    ],
  },
]
</script>

<style scoped>
.text-indigo {
  color: #3f51b5 !important;
}
.text-light-blue {
  color: #03a9f4 !important;
}
</style>

Positioning ​

align controls which side the menu opens on and how it aligns along the cross axis. Fine-tune with offsetX / offsetY.

When the menu is rendered inside its activator element, pass activator="parent" — CMenu binds activator listeners to the parent DOM element and does not render the activator slot.

Value
Side
Cross alignment
bottom
Below
Left-aligned
bottom-center
Below
Centered
bottom-right
Below
Right-aligned
top / top-center / top-right
Above
Left / Center / Right
right / right-center
Right
Top / Centered vertically
left / left-center
Left
Top / Centered vertically
Show code
html
<!-- Opens below the activator, left-aligned -->
<c-menu
  width="auto"
  align="bottom"
  :offset-y="8"
  open-on-click
  close-on-click-outside
  close-on-content-click
>
  <template #activator="{ on, activator }">
    <c-btn class="bg-indigo" v-bind="activator" v-on="on">Bottom</c-btn>
  </template>
  <c-card>...</c-card>
</c-menu>

<!-- Opens to the right, vertically centered -->
<c-menu width="auto" align="right-center" :offset-x="8" open-on-click close-on-click-outside>
  <template #activator="{ on, activator }">
    <c-btn class="bg-teal" v-bind="activator" v-on="on">Right</c-btn>
  </template>
  <c-card>...</c-card>
</c-menu>

<!-- Opens above, centered horizontally -->
<c-menu width="auto" align="top-center" :offset-y="8" open-on-click close-on-click-outside>
  <template #activator="{ on, activator }">
    <c-btn class="bg-deep-purple" v-bind="activator" v-on="on">Top center</c-btn>
  </template>
  <c-card>...</c-card>
</c-menu>

Context menu ​

Use position-x / position-y to anchor the menu to fixed coordinates instead of an activator element.

README.md
src
vite.config
package.json
styles.css
.env
Right-click anywhere to open context menu
Show code
vue
<template>
  <div class="area" @contextmenu.prevent="onContextMenu">Right-click anywhere</div>

  <c-menu
    v-model="open"
    :position-x="x"
    :position-y="y"
    width="auto"
    close-on-click-outside
    close-on-content-click
  >
    <c-card class="elevation-4" style="min-width:200px">
      <c-card-body class="py-1 px-0">
        <c-list>
          <c-list-item class="px-4" style="gap:12px" @click="toast('Opened')">
            <c-icon name="fas:eye" source="fa" :size="13" style="width:14px;opacity:.5" /> Open
          </c-list-item>
          <c-list-item class="px-4" style="gap:12px" @click="toast('Renamed')">
            <c-icon name="fas:pen" source="fa" :size="13" style="width:14px;opacity:.5" /> Rename
          </c-list-item>
          <c-list-item class="px-4" style="gap:12px" @click="toast('Copied')">
            <c-icon name="fas:link" source="fa" :size="13" style="width:14px;opacity:.5" /> Copy
            path
          </c-list-item>
        </c-list>
        <div class="sep" />
        <c-list>
          <c-list-item class="px-4" style="gap:12px;color:#f44336" @click="toast('Deleted')">
            <c-icon name="fas:trash" source="fa" :size="13" style="width:14px" /> Move to Trash
          </c-list-item>
        </c-list>
      </c-card-body>
    </c-card>
  </c-menu>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
const x = ref(0)
const y = ref(0)

function onContextMenu(e: MouseEvent) {
  open.value = false
  setTimeout(() => {
    x.value = e.pageX
    y.value = e.pageY
    open.value = true
  }, 0)
}

function toast(msg: string) {
  console.log(msg)
}
</script>

<style scoped>
.area {
  padding: 48px;
  text-align: center;
  border: 2px dashed var(--c-sys-color-outline-variant);
  border-radius: 8px;
}
.sep {
  height: 1px;
  background: var(--c-sys-color-outline-variant);
}
</style>

Width ​

By default CMenu inherits the width of its activator element. Pass width="auto" to let the content define its own width, or pass a fixed value.

html
<!-- Stretches to fill the activator (default) -->
<c-menu align="bottom">...</c-menu>

<!-- Content determines its own width -->
<c-menu align="bottom" width="auto">...</c-menu>

<!-- Fixed width -->
<c-menu align="bottom" :width="240">...</c-menu>

Collision strategies ​

strategy="reverse" flips to the opposite side when there is not enough space. strategy="bounce" keeps the menu inside the viewport by shifting it.

html
<!-- Flip above when there's no room below -->
<c-menu align="bottom" strategy="reverse" open-on-click>...</c-menu>

<!-- Stay inside the viewport edges -->
<c-menu align="bottom" strategy="bounce" open-on-click>...</c-menu>

v-model ​

Control the open state from outside the component.

vue
<template>
  <c-menu v-model="open" align="bottom">
    <template #activator="{ on, activator }">
      <button v-bind="activator" v-on="on">Toggle</button>
    </template>
    <div>Content</div>
  </c-menu>
  <button @click="open = !open">Toggle from outside</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>

ARIA ​

CMenu does not add any ARIA attributes on its own — each consumer is responsible for accessibility. Pass role and aria-* directly:

html
<c-menu role="menu" aria-label="Actions" open-on-click align="bottom"> ... </c-menu>

API ​

Props ​

Prop
Type
Default
Description
modelValue
boolean
false
Controls the open state
openOnClick
boolean
—
Open when the activator is clicked
closeOnClick
boolean
—
Toggle closed on repeated activator click
openOnHover
boolean
—
Open on mouseenter
closeOnLeave
boolean
—
Close on mouseleave
openOnFocus
boolean
—
Open when the activator receives focus
closeOnClickOutside
boolean
—
Close when clicking outside the menu
closeOnContentClick
boolean
—
Close when clicking inside the menu content
align
AlignValue
—
Side + cross-axis alignment. E.g. bottom, top-center, right-center, bottom-right
offsetX
number | string
—
Horizontal offset in px
offsetY
number | string
—
Vertical offset in px
positionX
number
—
Fixed X coordinate (detaches from activator)
positionY
number
—
Fixed Y coordinate (detaches from activator)
strategy
'reverse' | 'bounce'
—
Collision handling strategy
width
number | string
activator width
Content width
height
number | string
—
Content height
minWidth
number | string
—
Minimum content width
maxWidth
number | string
—
Maximum content width
minHeight
number | string
—
Minimum content height
maxHeight
number | string
—
Maximum content height
openDelay
number | string
—
Delay before opening (ms)
closeDelay
number | string
—
Delay before closing (ms)
transition
string
'fade'
Vue transition name applied to the content
ssr
boolean
—
Pre-render content on the server
activator
Element | ComponentPublicInstance | 'parent' | string
—
External activator, CSS selector, or parent element
preset
string
—
Registry path to a CMenuPreset (root zone, opened / closed states). Inside CSelect / CAutocomplete the menu also picks up the nested CMenuPreset from the combobox preset's menu field via context

AlignValue ​

ts
type AlignValue =
  | 'top'
  | 'top-center'
  | 'top-left'
  | 'top-right'
  | 'bottom'
  | 'bottom-center'
  | 'bottom-left'
  | 'bottom-right'
  | 'left'
  | 'left-center'
  | 'right'
  | 'right-center'

CSS variables ​

Variable
Default
Description
--c-menu-bg-color
var(--c-sys-color-surface-container-high)
Menu background
--c-menu-text-color
var(--c-sys-color-on-surface)
Text color
--c-menu-border-radius
var(--c-sys-shape-lg)
Container border radius
--c-menu-elevation
var(--c-sys-elevation-3)
Container shadow
--c-menu-max-height
100%
Maximum menu height

Slots ​

Slot
Props
Description
activator
{ on, activator }
The activator element
default
—
Menu content

activator slot props ​

Prop
Type
Description
on
ActivatorListeners
Event listeners — spread with v-on="on"
activator
Record<string, any>
Bind attrs — spread with v-bind="activator"

Events ​

Event
Arguments
Description
update:modelValue
boolean
Open state changed
open
—
Menu opened
close
—
Menu closed
click
—
Content clicked
outside-click
—
Click outside the menu

Expose ​

Method
Signature
Description
open
() => void
Open the menu
close
() => void
Close the menu
toggle
() => void
Toggle the menu