GkMenu
A floating menu teleported to body: v-model open state, #activator slot with props / is-open for aria-haspopup, aria-expanded, aria-controls, fixed top / left from useMenuAnchorPosition, outside mousedown dismiss, Escape to close, optional scrim, and closeOnContentClick.
This is intentionally smaller than Vuetify’s VMenu: no VOverlay activator stack, nested submenu parent chain, openDelay / closeDelay, locationStrategy / scrollStrategy, or Tab focus cycling through the whole document — submenu is reserved for a future phase.
When to use
Use for short action lists anchored to an activator (button, icon trigger, contextual controls). Keep menu items action-oriented and avoid long-form content.
Live Examples
Basic menu
Bind activator props to the trigger and use direct menu item children.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkMenu } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkMenu v-model="open">
<template #activator="{ props }">
<GkButton type="button" v-bind="props">Menu</GkButton>
</template>
<button type="button" class="item" role="menuitem" @click.stop="open = false">
Profile
</button>
<button type="button" class="item" role="menuitem">
Settings
</button>
</GkMenu>
</template>
<style scoped>
.item {
display: block;
width: 100%;
padding: var(--gk-space-2) var(--gk-space-3);
border: 0;
border-radius: var(--gk-radius-sm);
background: transparent;
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
.item:hover {
background: var(--gk-color-bg);
}
</style>Best practice: Use role='menuitem' on direct actionable children and only stop propagation when an item should keep the menu open.
Placement and offset
Use placement to align the menu relative to its activator.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkMenu } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkMenu v-model="open" placement="bottom-end" :offset="8">
<template #activator="{ props }">
<GkButton type="button" variant="secondary" v-bind="props">
bottom-end
</GkButton>
</template>
<button type="button" class="item" role="menuitem">Archive</button>
<button type="button" class="item" role="menuitem">Duplicate</button>
</GkMenu>
</template>
<style scoped>
.item {
display: block;
width: 100%;
padding: var(--gk-space-2) var(--gk-space-3);
border: 0;
border-radius: var(--gk-radius-sm);
background: transparent;
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
</style>Best practice: Prefer start/end placements that match the trigger edge; use offset for visual spacing only.
Persistent menu
Persistent menus require an explicit close action.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkMenu } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkMenu v-model="open" persistent show-scrim>
<template #activator="{ props }">
<GkButton type="button" variant="secondary" v-bind="props">
Persistent menu
</GkButton>
</template>
<p class="hint">Outside click and Escape stay open.</p>
<GkButton type="button" size="sm" slim @click="open = false">
Close
</GkButton>
</GkMenu>
</template>
<style scoped>
.hint {
margin: 0 0 var(--gk-space-2);
padding: var(--gk-space-2) var(--gk-space-3);
color: var(--gk-color-text-muted);
}
</style>Best practice: Use persistent sparingly; short action menus should normally dismiss on outside click or Escape.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | false | Open state; use v-model |
placement | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'bottom-start' | Panel position relative to the activator |
offset | number | 4 | Gap between activator and panel (px) |
closeOnContentClick | boolean | true | Clicking the panel closes the menu (use @click.stop on items to keep it open) |
showScrim | boolean | false | Full-screen scrim (uses --gk-menu-scrim) |
persistent | boolean | false | Outside click and Escape do not close |
disabled | boolean | false | Prevents opening |
to | string | HTMLElement | 'body' | Teleport target |
zIndex | number | string | --gk-menu-z-index | Layer stacking |
restoreFocus | boolean | true | Focus first focusable in the panel on open; restore previous focus on close |
submenu | boolean | false | Reserved (no nested menu wiring in v1) |
Additional attributes are applied to the panel (not the activator).
Slots
| Slot | Slot props | Description |
|---|---|---|
activator | props, is-open | Bind v-bind="props" on your control (typically type="button"). |
default | — | Menu content. For role="menu" on the panel, use direct role="menuitem" (or group/menuitemcheckbox) children per WAI-ARIA. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | boolean | Open state |
click:outside | MouseEvent | Outside mousedown (not emitted when persistent) |
afterEnter | — | Transition finished entering |
afterLeave | — | Transition finished leaving |
Composable
useMenuAnchorPosition is exported from god-kit/vue for custom anchored panels; GkMenuPlacement is the placement union type.
Tokens
| Token | Purpose |
|---|---|
--gk-menu-z-index | Layer stacking (2100) |
--gk-menu-min-width | Minimum panel width |
--gk-menu-max-height | max-height + scroll |
--gk-menu-shadow | Panel shadow |
--gk-menu-scrim | Scrim background (default transparent) |
Try It
Change menu placement and dismissal options, preview the result, and copy generated Vue code.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkMenu } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkMenu
v-model="open"
placement="bottom-start"
>
<template #activator="{ props }">
<GkButton type="button" v-bind="props">Menu</GkButton>
</template>
<button type="button" role="menuitem">Action</button>
</GkMenu>
</template>Accessibility notes
- Use
v-bind=\"props\"from the activator slot to preservearia-haspopup/aria-expandedwiring. - Keep focusable menu items as direct children with
role=\"menuitem\"(or related menuitem roles). - ArrowUp/ArrowDown and Home/End navigation should be tested in custom slot content.
Related components
Out of scope (v1)
Nested submenu registration, router integration, retainFocus / full Tab trap parity with Vuetify, openDelay / closeDelay, and scroll / location strategies beyond anchor + viewport clamping.
