Contextual overlays for lists, links and actions: one panel component with submenus, responsive placement and viewport-aware flipping.
Example
Added in v6.0.0The Vue Menu component is the contextual overlay the library builds its menus on. Dropdown is a compatibility surface over it, so new work belongs here.
<template>
<CMenu>
<CMenuToggle theme="secondary">Toggle menu</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
<CMenuItem href="#">Something else here</CMenuItem>
<CMenuDivider />
<CMenuItem href="#">Separated link</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuDivider, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Split button
Put the toggle beside a plain button in a button group to build a split menu; only the toggle opens the panel. Give the toggle split, which narrows its padding so the pair reads as one control.
<template>
<CButtonGroup>
<CButton theme="secondary">Split</CButton>
<CMenu>
<CMenuToggle theme="secondary" split>
<CIcon :icon="cilOptions" />
<span class="visually-hidden">Toggle menu</span>
</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</CButtonGroup>
</template>
<script setup>
import { CButton, CButtonGroup } from '@coreui/vue'
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
import { CIcon } from '@coreui/icons-vue'
import { cilOptions } from '@coreui/icons'
</script>Dark menus
Pass dark to render the panel in the dark theme, matching a dark navbar or a custom style.
<template>
<CMenu dark>
<CMenuToggle theme="secondary">Dark menu</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
<CMenuItem href="#">Something else here</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Translucent menus
Blur and saturate the background of menus to create a translucent effect with the translucent class on <CMenuContent>.
<template>
<CMenu>
<CMenuToggle theme="secondary">Menu button</CMenuToggle>
<CMenuContent translucent>
<CMenuItem class="active" href="#">Active menu item</CMenuItem>
<CMenuItem href="#">Menu item</CMenuItem>
<CMenuItem href="#">Menu item</CMenuItem>
<CMenuItem href="#">Menu item</CMenuItem>
<CMenuItem href="#">Menu item</CMenuItem>
<CMenuDivider />
<CMenuItem class="theme-danger" href="#">Danger menu item</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuDivider, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Placement
The placement property takes any Floating UI placement — bottom-start by default. The panel flips when the viewport runs out, and the -start/-end half of a placement is logical, so bottom-start opens from the right edge in a right-to-left context.
<template>
<div class="d-flex gap-1 flex-wrap">
<CMenu
v-for="placement in ['bottom-start', 'bottom-end', 'top-start', 'right-start']"
:key="placement"
:placement="placement"
>
<CMenuToggle theme="secondary">{{ placement }}</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</div>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Responsive
Breakpoint prefixes make a placement responsive: bottom-start md:top-end reads as bottom-start below md and top-end from md up. The breakpoints are the Sass ones — sm 576px, md 768px, lg 1024px, xl 1280px, 2xl 1536px.
<template>
<CMenu placement="bottom-start md:top-end">
<CMenuToggle theme="secondary">Responsive placement</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Offset
The offset property moves the panel away from its toggle, as [skidding, distance] in pixels — skidding along the toggle, distance away from it. It defaults to [0, 2].
<template>
<CMenu :offset="[10, 20]">
<CMenuToggle>Offset</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Teleport
The panel renders where it sits in the markup, so an ancestor with overflow: hidden, a transform or its own stacking context clips it — the left menu below is inside a box 80px tall. Pass portal to render the panel through a Teleport into body instead, and it escapes.
<template>
<div class="d-flex gap-4">
<div class="border rounded p-4 overflow-hidden" style="height: 80px">
<CMenu>
<CMenuToggle theme="secondary">Clipped</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
<CMenuItem href="#">Something else here</CMenuItem>
</CMenuContent>
</CMenu>
</div>
<div class="border rounded p-4 overflow-hidden" style="height: 80px">
<CMenu portal>
<CMenuToggle theme="secondary">Teleport</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
<CMenuItem href="#">Something else here</CMenuItem>
</CMenuContent>
</CMenu>
</div>
</div>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>container sends the panel somewhere other than the body — a CSS selector, an element, or a function returning one. Anything but body implies portal.
<CMenu container="#app-shell">Menu items
<CMenuItem> renders an <a> when it has an href and a <button> otherwise, so an action is focusable and the arrow keys reach it; pass as to render something else.
<template>
<CMenu>
<CMenuToggle theme="secondary">Menu items</CMenuToggle>
<CMenuContent>
<CMenuItem as="button">Action</CMenuItem>
<CMenuItem as="button">Another action</CMenuItem>
<CMenuItem as="button">Something else here</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Active
active styles the item as the current page and sets aria-current="page" for assistive technologies.
<template>
<CMenu>
<CMenuToggle theme="secondary">Active</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Regular link</CMenuItem>
<CMenuItem active href="#">Active link</CMenuItem>
<CMenuItem href="#">Another link</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Disabled
disabled styles the item as unavailable and stops it from being activated.
<template>
<CMenu>
<CMenuToggle theme="secondary">Disabled</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Regular link</CMenuItem>
<CMenuItem disabled href="#">Disabled link</CMenuItem>
<CMenuItem href="#">Another link</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Variants
Colour an individual item with a .theme-* class. The item reads the theme’s foreground while at rest and swaps to the theme’s fill with a contrasting label once it is active, so one class covers both states.
<template>
<div class="d-flex gap-4">
<CMenu>
<CMenuToggle theme="secondary">At rest</CMenuToggle>
<CMenuContent>
<CMenuItem v-for="theme in themes" :key="theme" :class="`theme-${theme}`" href="#">
{{ theme }} link
</CMenuItem>
</CMenuContent>
</CMenu>
<CMenu>
<CMenuToggle theme="secondary">Active</CMenuToggle>
<CMenuContent>
<CMenuItem v-for="theme in themes" :key="theme" active :class="`theme-${theme}`" href="#">
{{ theme }} link
</CMenuItem>
</CMenuContent>
</CMenu>
</div>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
const themes = ['primary', 'secondary', 'success', 'danger']
</script>Icons
Add .menu-item-icon directly to an <svg> to add an icon slot to menu items.
<template>
<CMenu>
<CMenuToggle theme="secondary">Icons</CMenuToggle>
<CMenuContent>
<CMenuItem as="button">
<svg
class="menu-item-icon fg-3"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"
/>
</svg>
Copy
</CMenuItem>
<CMenuItem as="button">
<svg
class="menu-item-icon fg-3"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M3.5 3.5c-.614-.884-.074-1.962.858-2.5L8 7.226 11.642 1c.932.538 1.472 1.616.858 2.5L8.81 8.61l1.556 2.661a2.5 2.5 0 1 1-.794.637L8 9.73l-1.572 2.177a2.5 2.5 0 1 1-.794-.637L7.19 8.61zm2.5 10a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0m7 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0"
/>
</svg>
Cut
</CMenuItem>
<CMenuDivider />
<CMenuItem as="button" class="theme-danger">
<svg class="menu-item-icon" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path
d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"
/>
</svg>
Delete
</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuDivider, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Descriptions
Use .menu-item-content for a flex column layout and .menu-item-description for secondary text below the main label.
<template>
<CMenu>
<CMenuToggle theme="secondary">Descriptions</CMenuToggle>
<CMenuContent>
<CMenuItem as="button">
<span class="menu-item-content">
Publish now
<small class="menu-item-description">Make this page visible immediately</small>
</span>
</CMenuItem>
<CMenuItem as="button">
<span class="menu-item-content">
Schedule
<small class="menu-item-description">Set a future publish date</small>
</span>
</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Selected
selected marks the current choice and sets aria-current. Give every item a .menu-item-check <svg> and only the selected one shows it, so the panel keeps its width as the choice moves.
<template>
<CMenu>
<CMenuToggle theme="secondary">Select status</CMenuToggle>
<CMenuContent>
<CMenuItem
v-for="option in statuses"
:key="option.value"
as="button"
:selected="status === option.value"
@click="status = option.value"
>
<span class="menu-item-content">
{{ option.label }}
<small class="menu-item-description">{{ option.description }}</small>
</span>
<svg
class="menu-item-check"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 16 16"
>
<path d="m2 7 4 5 8-8" />
</svg>
</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { ref } from 'vue'
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
const statuses = [
{ value: 'published', label: 'Published', description: 'Currently visible to users' },
{ value: 'draft', label: 'Draft', description: 'Not yet published' },
{ value: 'archived', label: 'Archived', description: 'Kept, but out of the way' },
]
const status = ref('published')
</script>Menu content
Headers
<CMenuHeader> labels a section of actions in any menu.
<template>
<CMenu>
<CMenuToggle theme="secondary">Headers</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Action</CMenuItem>
<CMenuHeader>Menu header</CMenuHeader>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuHeader, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Dividers
<CMenuDivider> separates groups of related menu items.
<template>
<CMenu>
<CMenuToggle theme="secondary">Dividers</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
<CMenuItem href="#">Something else here</CMenuItem>
<CMenuDivider />
<CMenuItem href="#">Separated link</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuDivider, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Text
<CMenuText> places non-interactive text in the panel. Anything else you nest works too — reach for spacing utilities, and expect to constrain the menu width.
<template>
<CMenu>
<CMenuToggle theme="secondary">Text</CMenuToggle>
<CMenuContent style="--cui-menu-min-width: 240px">
<CMenuText>Some example text that’s free-flowing within the menu.</CMenuText>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuText, CMenuToggle } from '@coreui/vue-pro'
</script>Forms
Put a form within a menu, or make it into a menu, and use spacing utilities to give it the negative space you require.
<template>
<CMenu auto-close="outside">
<CMenuToggle theme="secondary">Menu with form</CMenuToggle>
<CMenuContent :style="{ '--cui-menu-min-width': '300px' }">
<CForm class="d-flex flex-column gap-4 p-4">
<div>
<CFormLabel for="exampleMenuFormEmail1">Email address</CFormLabel>
<CFormInput id="exampleMenuFormEmail1" type="email" placeholder="[email protected]" />
</div>
<div>
<CFormLabel for="exampleMenuFormPassword1">Password</CFormLabel>
<CFormInput id="exampleMenuFormPassword1" type="password" placeholder="Password" />
</div>
<CFormField>
<CCheckbox id="exampleMenuFormCheck1" label="Remember me" />
</CFormField>
<div class="vstack gap-3">
<CButton theme="primary" type="submit">Sign in</CButton>
<CButton theme="secondary" variant="ghost" href="#">New around here? Sign up</CButton>
<CButton theme="secondary" variant="ghost" href="#">Forgot password?</CButton>
</div>
</CForm>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CButton, CCheckbox, CForm, CFormField, CFormInput, CFormLabel } from '@coreui/vue'
import { CMenu, CMenuContent, CMenuToggle } from '@coreui/vue-pro'
</script>Scrollable menus
Pass scrollable with a maxHeight to give a long panel its own scrollbar.
<template>
<CMenu>
<CMenuToggle theme="secondary">Scrollable menu</CMenuToggle>
<CMenuContent :max-height="200" scrollable>
<CMenuItem v-for="index in 20" :key="index" href="#">Action {{ index }}</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Submenus
<CSubmenu> nests a menu inside a menu. Its toggler becomes the parent item, and the nested panel opens on hover and on click. Below 576px a submenu stacks over the parent panel with a back button instead of flying out to the side.
<template>
<CMenu>
<CMenuToggle theme="secondary">Submenus</CMenuToggle>
<CMenuContent>
<CSubmenu toggler="File">
<CMenuItem href="#">New</CMenuItem>
<CMenuItem href="#">Open</CMenuItem>
<CMenuItem href="#">Save</CMenuItem>
</CSubmenu>
<CSubmenu toggler="Edit">
<CMenuItem href="#">Cut</CMenuItem>
<CMenuItem href="#">Copy</CMenuItem>
<CMenuItem href="#">Paste</CMenuItem>
</CSubmenu>
<CSubmenu toggler="View">
<CMenuItem href="#">Zoom In</CMenuItem>
<CMenuItem href="#">Zoom Out</CMenuItem>
</CSubmenu>
<CMenuDivider />
<CMenuItem href="#">Preferences</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import {
CMenu,
CMenuContent,
CMenuDivider,
CMenuItem,
CMenuToggle,
CSubmenu,
} from '@coreui/vue-pro'
</script>Nested
Submenus nest to any depth. Each level opens to the side and flips direction when the viewport runs out.
<template>
<CMenu>
<CMenuToggle theme="secondary">Multi-level menu</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Level 1 action</CMenuItem>
<CSubmenu toggler="Level 1 submenu">
<CMenuItem href="#">Level 2 action</CMenuItem>
<CSubmenu toggler="Level 2 submenu">
<CMenuItem href="#">Level 3 action A</CMenuItem>
<CMenuItem href="#">Level 3 action B</CMenuItem>
</CSubmenu>
<CMenuItem href="#">Another level 2</CMenuItem>
</CSubmenu>
<CMenuItem href="#">Another level 1</CMenuItem>
</CMenuContent>
</CMenu>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle, CSubmenu } from '@coreui/vue-pro'
</script>Auto close behavior
By default a menu closes on a click inside or outside it. autoClose narrows that to 'inside' or 'outside', and false leaves only the toggle. Escape always closes the menu and returns focus to the toggle, whatever the setting.
<template>
<div class="d-flex gap-1 flex-wrap">
<CMenu
v-for="autoClose in [true, 'inside', 'outside', false]"
:key="String(autoClose)"
:auto-close="autoClose"
>
<CMenuToggle theme="secondary">autoClose: {{ autoClose }}</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
</CMenuContent>
</CMenu>
</div>
</template>
<script setup>
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
</script>Usage with v-model
Pass show and the menu renders what you pass — the toggle and the dismiss gestures only emit @update:show, so feed the change back, or nothing moves. The v-model:show binding is that loop in one attribute. Here an external button drives the same state the menu’s own gestures request changes to.
<template>
<div class="d-flex gap-3">
<CButton theme="secondary" variant="outline" @click="show = !show">
{{ show ? 'Close the menu' : 'Open the menu' }}
</CButton>
<CMenu v-model:show="show">
<CMenuToggle theme="secondary">Toggle menu</CMenuToggle>
<CMenuContent>
<CMenuItem href="#">Action</CMenuItem>
<CMenuItem href="#">Another action</CMenuItem>
<CMenuItem href="#">Something else here</CMenuItem>
</CMenuContent>
</CMenu>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { CButton } from '@coreui/vue'
import { CMenu, CMenuContent, CMenuItem, CMenuToggle } from '@coreui/vue-pro'
const show = ref(false)
</script>Use it when the open state belongs to something else — a command palette shortcut, a tour step, or a control outside the menu like the button above.
Initial open state
Without a binding the toggle manages the panel. Pass show to seed it open on mount.
<CMenu show>@update:show fires either way whenever the menu requests an open-state change — a dismiss gesture (Escape, an outside click, an item click) or any internal toggle. The deprecated visible prop keeps its old behavior and is ignored when show is set.
Accessibility
The toggle carries aria-haspopup and aria-expanded, the panel role="menu", and each item role="menuitem"; a selected item is aria-current.
Keyboard navigation
| Key | Action |
|---|---|
↓ / ↑ | Move focus to the next/previous item — from the toggle, open the panel and step into it |
→ | Open the focused submenu |
Enter / Space | Activate the focused item, or open its submenu |
Esc | Close the menu and return focus to the toggle |
API
Check out the documentation below for a comprehensive guide to all the props you can use with the components mentioned here.