Popover Menu
UiPopoverMenu shows a dropdown panel with three slots — header, body and footer — plus UiPopoverMenuGroup and UiPopoverMenuItem for the rows inside it, and UiPopoverMenuUser for an identity header. Every region is optional: the panel renders only the parts you fill.
Add a popover menu
Fill #body with UiPopoverMenuGroup blocks of UiPopoverMenuItem rows, separated by UiDivider.
<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<UiPopoverMenu
id="account-menu"
:is-opened="open"
title="Account"
aria-label="Account menu"
@hide-dropdown="open = false"
>
<template #body>
<UiPopoverMenuGroup>
<UiPopoverMenuItem :as="NuxtLink" to="/dashboard" icon="" text="Dashboard" />
<UiPopoverMenuItem :as="NuxtLink" to="/settings" icon="" text="Settings" />
</UiPopoverMenuGroup>
<UiDivider />
<UiPopoverMenuGroup>
<UiPopoverMenuItem icon="" text="Sign out" is-destructive @click="signOut" />
</UiPopoverMenuGroup>
</template>
</UiPopoverMenu>
</template>
The header
By default the header is a title and a close button, and the close button is pinned to the top right whatever the header holds — including a custom header slot, so you never re-add it. Set closable to false to drop it; with no title, no header slot and no close button, the header disappears.
For an identity header, put UiPopoverMenuUser in the slot. It lays the avatar beside the name and email, and takes user (any { displayName, email, photoURL } object — the Firebase shape) or the displayName / email / photoUrl props individually.
<template>
<UiPopoverMenu :is-opened="open" @hide-dropdown="open = false">
<template #header>
<UiPopoverMenuUser :user="user">
<template #trailing>
<UiBadge text="Pro" variant="outline" size="sm" />
</template>
</UiPopoverMenuUser>
</template>
</UiPopoverMenu>
</template>
Its avatar slot replaces the image — for UiAvatar with initials, a mask or a status dot — and its default slot replaces both text lines:
<template>
<UiPopoverMenuUser display-name="Colorffy" email="Enterprise workspace">
<template #avatar>
<UiAvatar initials="CO" size="md" status="online" />
</template>
</UiPopoverMenuUser>
</template>
Menu items
UiPopoverMenuItem renders a button by default, an a when as="a", or a router component when you pass one — so actions, external links and in-app navigation share one row style. Inside the menu, ↓ and ↑ move between items and wrap, Home and End jump to the ends, and a letter moves to the next item starting with it.
<template>
<!-- Action -->
<UiPopoverMenuItem icon="" text="Command menu" shortcut="⌘K" @click="openPalette" />
<!-- External link -->
<UiPopoverMenuItem as="a" to="https://example.com" text="Documentation" icon-trailing="" />
<!-- Router link — it sets its own active class, so leave `active` unset -->
<UiPopoverMenuItem :as="NuxtLink" to="/projects" icon="" text="Projects" />
<!-- Destructive, disabled, badged -->
<UiPopoverMenuItem icon="" text="Delete" is-destructive />
<UiPopoverMenuItem text="Billing" disabled />
<UiPopoverMenuItem text="Members" :badge="{ text: '12', variant: 'primary', pill: true }" />
</template>
The trailing slot replaces the badge / shortcut / trailing-icon area entirely, which is how you put a control inside a row — a theme switch, for example. Use as="div" there so the row is a container rather than a menu item:
<template>
<UiPopoverMenuItem as="div" icon="" text="Theme">
<template #trailing>
<UiButtonGroup connected joined>
<UiButton icon size="sm" aria-label="Light" @click="setTheme('light')" />
<UiButton icon size="sm" aria-label="Dark" @click="setTheme('dark')" />
</UiButtonGroup>
</template>
</UiPopoverMenuItem>
</template>
Footer
The footer is a flex row, so use flex-grow-1 to push trailing content to the end:
<template>
<UiPopoverMenu :is-opened="open" @hide-dropdown="open = false">
<template #footer>
<span class="subtitle-2 text-muted flex-grow-1">Colorffy UI</span>
<span class="subtitle-2 text-muted">v2.5.0</span>
</template>
</UiPopoverMenu>
</template>
Menu items shortcut
For a plain list of rows, menuItems renders the body for you — wrapped in a single group. Entries accept everything UiPopoverMenuItem takes, plus an id. A filled body slot wins when both are present.
<template>
<UiPopoverMenu :is-opened="open" :menu-items="items" :current-route="route" @menu-item-click="to => navigateTo(to)" />
</template>
Native popover
When the browser supports both the Popover API and CSS anchor positioning, the panel renders as a native popover="auto": it is promoted to the top layer, anchored to its place in the navbar, and the browser handles click-outside and Esc dismissal itself — the component emits hideDropdown so isOpened stays in sync. No polyfill is loaded.
Browsers missing either feature — and any render with native-popover="false" — keep the class-based rendering, where the panel positions itself absolutely inside the navbar. In that branch dismissal stays yours: keep a click-outside handler (e.g. v-on-click-outside) that sets isOpened to false. It is harmless alongside the native branch, so you only drop it when every target browser has both features.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
isOpened | boolean | false | Show / hide the menu. |
id | string | null | — | DOM id. Pass one when a page has more than one menu. |
ariaLabel | string | null | 'Menu' | Accessible name for the panel. |
closable | boolean | true | Close button, pinned to the header's top right. |
nativePopover | boolean | true | Render through the native Popover API when the browser supports it and CSS anchor positioning. false forces the class-based rendering. |
menuItems | IPopoverMenuItem[] | [] | Body rows, when no body slot is filled. |
title | string | null | — | Default header title. |
currentRoute | { path?, name? } | null | — | Active-row detection for menuItems (pass useRoute()). |
Slots: header, body, footer. A filled body replaces the menuItems rows.
Emits: hideDropdown, menuItemClick(to).
UiPopoverMenuUser props
| Prop | Type | Default | Description |
|---|---|---|---|
user | { displayName, email, photoURL } | null | — | Fills all three fields at once. |
displayName / email / photoUrl | string | null | — | Individual fields; each wins over user. |
alt | string | null | display name | Avatar alt text. |
avatarClass | string | string[] | null | — | Classes for the avatar. |
customClass | string | string[] | null | — | Classes for the block. |
Slots: avatar, default (the two text lines), trailing.
UiPopoverMenuGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | null | — | Label above the rows; also the group's accessible name. |
ariaLabel | string | null | — | Accessible name when there is no visible label. |
customClass | string | string[] | null | — | Classes for the group. |
Renders role="group", so give each group a text or ariaLabel when a menu has more than one.
UiPopoverMenuItem props
| Prop | Type | Default | Description |
|---|---|---|---|
as | string | object | 'button' | 'button', 'a', 'div', or a router component. |
text | string | '' | Label. The default slot overrides it. |
icon / iconTrailing | string | null | — | Material icon codes, leading and trailing. |
iconClass | string | string[] | null | — | Classes for the leading icon. |
iconStyle | string | Record<string, string | number> | null | — | Inline styles for the leading icon. |
to | string | object | null | — | Destination for link and router variants. |
active | boolean | false | Marks the current row. Router components handle this themselves. |
disabled | boolean | false | Blocks activation. |
isDestructive | boolean | false | Danger styling. |
shortcut | string | null | — | Trailing keyboard hint. |
badge | Partial<IBadgeProps> | null | — | Trailing badge. |
ariaLabel | string | null | — | Accessible name when the visible text is not descriptive enough. |
customClass | string | string[] | null | — | Classes for the item. |
Slots: default (label), trailing. Emits: click(event).
div or span are treated as containers, not menu items: they carry no menuitem role, type or href. They still emit click, so a click on an embedded control bubbles up to the row.UiButtonMenu instead — it positions itself against its trigger.CSS variables
Set these on :root, or on any wrapper to restyle only the menus inside it. Destructive items keep their danger color. Hover and pressed backgrounds come from the shared state layer.
:root {
--cffy-popover-menu-radius: var(--cffy-radius-xl);
--cffy-popover-menu-item-active-bg-color: var(--cffy-primary-a10);
--cffy-popover-menu-item-active-color: var(--cffy-on-primary);
}
| Variable | Default | Controls |
|---|---|---|
--cffy-popover-menu-bg-color / --cffy-popover-menu-color | --cffy-surface-overlay / --cffy-on-background | Panel background and text |
--cffy-popover-menu-border-color | --cffy-outline-overlay | Panel border |
--cffy-popover-menu-radius | --cffy-shape-container (12px) | Panel corners (items use half) |
--cffy-popover-menu-shadow | --cffy-shadow-xl | Panel shadow |
--cffy-popover-menu-padding | --cffy-space-6 | Panel padding |
--cffy-popover-menu-min-width | --cffy-container-2xs (18rem) | Panel minimum width |
--cffy-popover-menu-spacing | --cffy-space-16 | Offset from the trigger and the spacing between sections |
--cffy-popover-menu-easing / --cffy-popover-menu-duration | --cffy-ease-spring / --cffy-duration-800 | Opening animation |
--cffy-popover-menu-item-color | --cffy-muted-a10 | Item text and icon |
--cffy-popover-menu-item-font-size | --cffy-fs-3xs | Item text size |
--cffy-popover-menu-item-radius | half the panel radius | Item corners |
--cffy-popover-menu-item-hover-color | --cffy-primary-a30 | Hovered item text |
--cffy-popover-menu-item-active-color / --cffy-popover-menu-item-active-bg-color | --cffy-on-primary-container / primary tint | Current-route item |