Anchored floating panel for actions and dropdowns.
Introduction
Menus display a list of choices on a temporary surface. They appear when the user interacts with a trigger—a button, icon button, or other control—and close after a selection or when dismissed.
Menu vs Select: Use Menu for action lists and commands (Edit, Delete, Share) where each item triggers behavior. Use Select for form fields where the user picks one or more values from a predefined set, with label, validation, and controlled value state. Menus close after a typical action; Select stays open for multi-select and integrates with form field chrome.
Compose menu content with a List of ListItem rows inside the panel.
Import
import { Menu } from "@bridge-ui/vue/Components/Menu";import { Menu } from "@bridge-ui/react/Components/Menu";Basic usage
Provide a trigger via the trigger slot (Vue) or slots.trigger (React). Put menu content such as a List with ListItem elements inside the panel.
<script setup lang="ts">
import { ref } from "vue";
import { Button } from "@bridge-ui/vue/Components/Button";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { Menu } from "@bridge-ui/vue/Components/Menu";
const open = ref(false);
const close = () => {
open.value = false;
};
</script>
<template>
<div class="w-full">
<Menu v-model="open">
<template #trigger>
<Button>Open menu</Button>
</template>
<List dense>
<ListItem
interactive
primary="Edit"
role="menuitem"
:custom-props="{ interactive: { onClick: close } }"
/>
<ListItem
interactive
role="menuitem"
primary="Duplicate"
:custom-props="{ interactive: { onClick: close } }"
/>
<ListItem
interactive
role="menuitem"
primary="Delete"
:custom-props="{ interactive: { onClick: close } }"
/>
</List>
</Menu>
</div>
</template>
import { useState } from "react";
import { Button } from "@bridge-ui/react/Components/Button";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { Menu } from "@bridge-ui/react/Components/Menu";
export default function MenuBasic() {
const [open, setOpen] = useState(false);
const close = () => setOpen(false);
return (
<div className="w-full">
<Menu
show={open}
onShowChange={setOpen}
slots={{ trigger: <Button>Open menu</Button> }}
>
<List dense>
<ListItem
interactive
primary="Edit"
role="menuitem"
customProps={{ interactive: { onClick: close } }}
/>
<ListItem
interactive
role="menuitem"
primary="Duplicate"
customProps={{ interactive: { onClick: close } }}
/>
<ListItem
interactive
role="menuitem"
primary="Delete"
customProps={{ interactive: { onClick: close } }}
/>
</List>
</Menu>
</div>
);
}
External anchor
Pass anchorEl to position against an element outside the menu instead of using the trigger slot. Control visibility with show / v-model and toggle the opener yourself.
<script setup lang="ts">
import { ref, useTemplateRef } from "vue";
import { Button } from "@bridge-ui/vue/Components/Button";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { Menu } from "@bridge-ui/vue/Components/Menu";
const open = ref(false);
const anchorEl = useTemplateRef<HTMLElement>("anchorEl");
const close = () => {
open.value = false;
};
</script>
<template>
<div class="w-full">
<span ref="anchorEl" class="inline-flex">
<Button
aria-haspopup="menu"
:aria-expanded="open"
v-on:click="open = !open"
>
Open menu
</Button>
</span>
<Menu v-model="open" :anchor-el="anchorEl">
<List dense>
<ListItem
interactive
primary="Edit"
role="menuitem"
:custom-props="{ interactive: { onClick: close } }"
/>
<ListItem
interactive
role="menuitem"
primary="Duplicate"
:custom-props="{ interactive: { onClick: close } }"
/>
<ListItem
interactive
role="menuitem"
primary="Delete"
:custom-props="{ interactive: { onClick: close } }"
/>
</List>
</Menu>
</div>
</template>
import { useRef, useState } from "react";
import { Button } from "@bridge-ui/react/Components/Button";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { Menu } from "@bridge-ui/react/Components/Menu";
export default function MenuAnchorEl() {
const [open, setOpen] = useState(false);
const anchorRef = useRef<HTMLSpanElement>(null);
const close = () => setOpen(false);
return (
<div className="w-full">
<span ref={anchorRef} className="inline-flex">
<Button
aria-expanded={open}
aria-haspopup="menu"
onClick={() => setOpen((value) => !value)}
>
Open menu
</Button>
</span>
<Menu show={open} anchorEl={anchorRef} onShowChange={setOpen}>
<List dense>
<ListItem
interactive
primary="Edit"
role="menuitem"
customProps={{ interactive: { onClick: close } }}
/>
<ListItem
interactive
role="menuitem"
primary="Duplicate"
customProps={{ interactive: { onClick: close } }}
/>
<ListItem
interactive
role="menuitem"
primary="Delete"
customProps={{ interactive: { onClick: close } }}
/>
</List>
</Menu>
</div>
);
}
Placement
Use the placement prop to position the panel relative to the trigger. The default is bottom-start. This example uses top-start.
<script setup lang="ts">
import { ref } from "vue";
import { Button } from "@bridge-ui/vue/Components/Button";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { Menu } from "@bridge-ui/vue/Components/Menu";
const open = ref(false);
const close = () => {
open.value = false;
};
</script>
<template>
<div class="w-full">
<Menu v-model="open" placement="top-start">
<template #trigger>
<Button>Open menu</Button>
</template>
<List dense>
<ListItem
interactive
role="menuitem"
primary="Profile"
:custom-props="{ interactive: { onClick: close } }"
/>
<ListItem
interactive
role="menuitem"
primary="Settings"
:custom-props="{ interactive: { onClick: close } }"
/>
</List>
</Menu>
</div>
</template>
import { useState } from "react";
import { Button } from "@bridge-ui/react/Components/Button";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { Menu } from "@bridge-ui/react/Components/Menu";
export default function MenuPlacement() {
const [open, setOpen] = useState(false);
const close = () => setOpen(false);
return (
<div className="w-full">
<Menu
show={open}
placement="top-start"
onShowChange={setOpen}
slots={{ trigger: <Button>Open menu</Button> }}
>
<List dense>
<ListItem
interactive
role="menuitem"
primary="Profile"
customProps={{ interactive: { onClick: close } }}
/>
<ListItem
interactive
role="menuitem"
primary="Settings"
customProps={{ interactive: { onClick: close } }}
/>
</List>
</Menu>
</div>
);
}
Persistent
When persistent is true, Escape and click-away do not close the menu. Toggle it closed programmatically or via the trigger.
<script setup lang="ts">
import { ref } from "vue";
import { Button } from "@bridge-ui/vue/Components/Button";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { Menu } from "@bridge-ui/vue/Components/Menu";
const open = ref(false);
</script>
<template>
<div class="w-full">
<Menu persistent v-model="open">
<template #trigger>
<Button>Open persistent menu</Button>
</template>
<List dense>
<ListItem interactive role="menuitem" primary="Item one" />
<ListItem interactive role="menuitem" primary="Item two" />
</List>
</Menu>
</div>
</template>
import { useState } from "react";
import { Button } from "@bridge-ui/react/Components/Button";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { Menu } from "@bridge-ui/react/Components/Menu";
export default function MenuPersistent() {
const [open, setOpen] = useState(false);
return (
<div className="w-full">
<Menu
persistent
show={open}
onShowChange={setOpen}
slots={{ trigger: <Button>Open persistent menu</Button> }}
>
<List dense>
<ListItem interactive role="menuitem" primary="Item one" />
<ListItem interactive role="menuitem" primary="Item two" />
</List>
</Menu>
</div>
);
}
Dismiss behavior
Fine-tune closing with closeOnEscape, closeOnClickAway, and the @close / onClose event. Use rounded and shadow to style the panel.
<script setup lang="ts">
import { ref } from "vue";
import { Button } from "@bridge-ui/vue/Components/Button";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { Menu } from "@bridge-ui/vue/Components/Menu";
const escapeOpen = ref(false);
const clickAwayOpen = ref(false);
const closeCount = ref(0);
const onMenuClose = () => {
closeCount.value += 1;
};
</script>
<template>
<div class="flex flex-col gap-6">
<Menu
shadow="lg"
rounded="2xl"
v-model="escapeOpen"
:close-on-escape="false"
>
<template #trigger>
<Button size="sm" variant="outline">closeOnEscape=false</Button>
</template>
<List dense>
<ListItem primary="Escape does not close this menu" />
</List>
</Menu>
<Menu
v-model="clickAwayOpen"
v-on:close="onMenuClose"
:close-on-click-away="false"
>
<template #trigger>
<Button size="sm" variant="outline">closeOnClickAway=false</Button>
</template>
<List dense>
<ListItem primary="Click outside does not close" />
<ListItem :secondary="`@close fired ${closeCount} times`" />
</List>
</Menu>
</div>
</template>
import { useState } from "react";
import { Button } from "@bridge-ui/react/Components/Button";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { Menu } from "@bridge-ui/react/Components/Menu";
const MenuAdvanced = () => {
const [escapeOpen, setEscapeOpen] = useState(false);
const [clickAwayOpen, setClickAwayOpen] = useState(false);
const [closeCount, setCloseCount] = useState(0);
return (
<div className="flex flex-col gap-6">
<Menu
shadow="lg"
rounded="2xl"
show={escapeOpen}
closeOnEscape={false}
onShowChange={setEscapeOpen}
slots={{
trigger: (
<Button size="sm" variant="outline">
closeOnEscape=false
</Button>
),
}}
>
<List dense>
<ListItem primary="Escape does not close this menu" />
</List>
</Menu>
<Menu
show={clickAwayOpen}
closeOnClickAway={false}
onShowChange={setClickAwayOpen}
onClose={() => setCloseCount((c) => c + 1)}
slots={{
trigger: (
<Button size="sm" variant="outline">
closeOnClickAway=false
</Button>
),
}}
>
<List dense>
<ListItem primary="Click outside does not close" />
<ListItem secondary={`onClose fired ${closeCount} times`} />
</List>
</Menu>
</div>
);
};
export default MenuAdvanced;
Related components
Accessibility
- Provide the trigger via the trigger slot with a button that has an accessible name.
- On open, focus moves to the first focusable item unless disableAutoFocus is set.
- Escape closes the menu when closeOnEscape is true (default).
- Click-away closes the menu when closeOnClickAway is true (default).
- Set role="menuitem" on ListItem rows inside the panel for screen reader context.
Anatomy
<div> <!-- Menu root -->
<button> <!-- trigger slot -->
<div> <!-- portaled panel -->
<ul> <!-- List content -->
<li> <!-- ListItem rows -->
</ul>
</div>
</div>The panel is portaled to teleportTo (default "body") and positioned relative to the trigger or anchorEl.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| v-model | boolean | — | Two-way binding for overlay visibility. |
| @close | — | — | Fired when the user dismisses (Escape or click-away). Not fired when the parent sets v-model={false} directly. |
| @show-change | (show: boolean) => void | — | Equivalent to onShowChange when you prefer events over a callback prop. |
| Prop | Type | Default | Description |
|---|---|---|---|
| show | boolean | — | Whether the overlay is visible. Use with onShowChange for controlled state. |
| onShowChange | (show: boolean) => void | — | Called when visibility changes. |
| onClose | () => void | — | Fired when the user dismisses (Escape or click-away). Not fired when the parent sets show={false} directly. |
| Prop | Type | Default | Description |
|---|---|---|---|
| anchorEl | HTMLElement | RefObject<HTMLElement | null> | null | — | External anchor element for positioning. |
| placement | PositionPlacement | "bottom-start" | Preferred placement relative to the anchor. |
| offset | number | 4 | Gap between trigger and panel (px). |
| persistent | boolean | false | When true, Escape and click-away do not close the menu. |
| closeOnClickAway | boolean | true | Whether the menu closes when clicking outside. |
| closeOnEscape | boolean | true | Whether the menu closes on Escape. |
| rounded | MenuRounded | "md" | Border radius of the menu panel. |
| shadow | MenuShadow | "md" | Shadow applied to the menu panel. |
| strategy | PositionStrategy | "fixed" | CSS position strategy for the floating panel. |
| teleportTo | string | false | "body" | Portal target. Pass false to render in place. |
| keepMounted | boolean | false | When true, the menu stays mounted after closing (hidden). |
| disableAutoFocus | boolean | false | When true, the menu does not auto-focus the first focusable item on open. |
| disableScrollLock | boolean | true | When true, body scroll is not locked while the menu is open. |
| slots | MenuSlots | — | Slot overrides. Use trigger for the opener element. |
| classes | MenuClasses | — | Class overrides for menu parts. |
| customProps | MenuCustomProps | — | Props forwarded to each menu part. |