Components

Popover Menu

A dropdown panel with header, body and footer slots, built from popover menu items.

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="&#xe871;" text="Dashboard" />
        <UiPopoverMenuItem :as="NuxtLink" to="/settings" icon="&#xe8b8;" text="Settings" />
      </UiPopoverMenuGroup>

      <UiDivider />

      <UiPopoverMenuGroup>
        <UiPopoverMenuItem icon="&#xe879;" 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>

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="&#xe8b8;" text="Command menu" shortcut="⌘K" @click="openPalette" />

  <!-- External link -->
  <UiPopoverMenuItem as="a" to="https://example.com" text="Documentation" icon-trailing="&#xe89e;" />

  <!-- Router link — it sets its own active class, so leave `active` unset -->
  <UiPopoverMenuItem :as="NuxtLink" to="/projects" icon="&#xe8ef;" text="Projects" />

  <!-- Destructive, disabled, badged -->
  <UiPopoverMenuItem icon="&#xe879;" 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="&#xe3ac;" 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>

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>

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

PropTypeDefaultDescription
isOpenedbooleanfalseShow / hide the menu.
idstring | null—DOM id. Pass one when a page has more than one menu.
ariaLabelstring | null'Menu'Accessible name for the panel.
closablebooleantrueClose button, pinned to the header's top right.
nativePopoverbooleantrueRender through the native Popover API when the browser supports it and CSS anchor positioning. false forces the class-based rendering.
menuItemsIPopoverMenuItem[][]Body rows, when no body slot is filled.
titlestring | 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

PropTypeDefaultDescription
user{ displayName, email, photoURL } | null—Fills all three fields at once.
displayName / email / photoUrlstring | null—Individual fields; each wins over user.
altstring | nulldisplay nameAvatar alt text.
avatarClassstring | string[] | null—Classes for the avatar.
customClassstring | string[] | null—Classes for the block.

Slots: avatar, default (the two text lines), trailing.

UiPopoverMenuGroup props

PropTypeDefaultDescription
textstring | null—Label above the rows; also the group's accessible name.
ariaLabelstring | null—Accessible name when there is no visible label.
customClassstring | 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

PropTypeDefaultDescription
asstring | object'button''button', 'a', 'div', or a router component.
textstring''Label. The default slot overrides it.
icon / iconTrailingstring | null—Material icon codes, leading and trailing.
iconClassstring | string[] | null—Classes for the leading icon.
iconStylestring | Record<string, string | number> | null—Inline styles for the leading icon.
tostring | object | null—Destination for link and router variants.
activebooleanfalseMarks the current row. Router components handle this themselves.
disabledbooleanfalseBlocks activation.
isDestructivebooleanfalseDanger styling.
shortcutstring | null—Trailing keyboard hint.
badgePartial<IBadgeProps> | null—Trailing badge.
ariaLabelstring | null—Accessible name when the visible text is not descriptive enough.
customClassstring | string[] | null—Classes for the item.

Slots: default (label), trailing. Emits: click(event).

Rows rendered as 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.
For action menus attached to a button, use 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);
}
VariableDefaultControls
--cffy-popover-menu-bg-color / --cffy-popover-menu-color--cffy-surface-overlay / --cffy-on-backgroundPanel background and text
--cffy-popover-menu-border-color--cffy-outline-overlayPanel border
--cffy-popover-menu-radius--cffy-shape-container (12px)Panel corners (items use half)
--cffy-popover-menu-shadow--cffy-shadow-xlPanel shadow
--cffy-popover-menu-padding--cffy-space-6Panel padding
--cffy-popover-menu-min-width--cffy-container-2xs (18rem)Panel minimum width
--cffy-popover-menu-spacing--cffy-space-16Offset from the trigger and the spacing between sections
--cffy-popover-menu-easing / --cffy-popover-menu-duration--cffy-ease-spring / --cffy-duration-800Opening animation
--cffy-popover-menu-item-color--cffy-muted-a10Item text and icon
--cffy-popover-menu-item-font-size--cffy-fs-3xsItem text size
--cffy-popover-menu-item-radiushalf the panel radiusItem corners
--cffy-popover-menu-item-hover-color--cffy-primary-a30Hovered item text
--cffy-popover-menu-item-active-color / --cffy-popover-menu-item-active-bg-color--cffy-on-primary-container / primary tintCurrent-route item
Copyright © 2026