22 KiB
import Popover from "~/components/ui/Popover.vue"
Popover (Dropdown Menu)
Popovers (Popover
) are dropdown menus activated by a button. Use popovers to create dropdown menus ranging from simple lists to complex nested menus.
::: warning A11y issues
This component has severe usability issues and cannot be used as-is.
Add keyboard operability
I can't operate the popup with a keyboard. Remove barrier for people not using a mouse.
-
✅ All items can be focused and activated by
Space
(use<button>
element instead of<div>
) -
Tab order: Users can go to the next and previous items with
Tab
andShift+Tab
. When they have opened a sub-menu, the next focusable element is inside the sub-menu. When they have closed it, the focus jumps back to where it was. -
Dismissal: Users can close a menu or sub-menu with
ESC
-
Arrow keys: Users can move up and down a menu, open sub-menus with
->
and close them with<-
. I have added something like this toTabs.vue
, for reference.
Implement expected behavior
Switching to submenus is error-prone. When moving cursor into freshly opened submenu, it should not close if the cursor crosses another menu item.

- Dead triangle: Add a triangular invisible node that covers the possible paths from the current mouse position to the menu items.
Large menus disappear. I can't scroll to see all options.
- Submenu-to-Modal: Lists longer than 12 or so items are not recommended and should be replaced with modals.
Submenus open without a delay, and they don't close unless I click somewhere outside them, which goes against established expectations.
-
Expansion delay: Sub-menus open after 200ms
-
Auto-close: Sub-menus close when the outside is hovered. There may be a delay of 200ms. Menus close when they lose focus.
Common UI libraries in the Vue ecosystem such as vuetify or shadcn-vue all implement these features. It may be prudent to use their components.
::: tip Quick mitigation tactics:
- Place complex interfaces into nested
Modal
s - Place long lists into native
<Select>
elements - Avoid sub-menus
:::
Common uses:
- "More actions" dropdown menus
- Navigation menus
- Settings menus
- Context menus (right-click menus)
Prop | Data type | Required? | Description |
---|---|---|---|
open |
Boolean | No | Controls whether the popover is open. Defaults to false . |
<Popover>
<template #default="{ toggleOpen }">
<OptionsButton @click="toggleOpen" />
</template>
</Popover>
Destructure the function toggleOpen
and let
a default dropdown button: OptionsButton
trigger it. This way, the state of the component is encapsulated.
Bind to isOpen
If you want to process or influence the expansion of the menu in the containing component, you can bind it to a ref
.
Use Vue event handling to map the button to a boolean value.
<script setup lang="ts">
const isOpen = ref(false)
</script>
<template>
<Popover v-model="isOpen">
<OptionsButton @click="isOpen = !isOpen" />
</Popover>
</template>
Customize the dropdown button
<script setup lang="ts">
const open = ref(false);
const privacyChoices = ["pod", "public", "private"];
const bcPrivacy = ref("pod");
</script>
<template>
<Popover v-model="isOpen">
<template #default="{ toggleOpen }">
<Pill
@click="
(e) => {
console.log('Pill clicked');
console.log('Before toggleOpen:', isOpen);
toggleOpen();
console.log('After toggleOpen:', isOpen);
}
"
:blue="bcPrivacy === 'pod'"
:red="bcPrivacy === 'public'"
>
{{ bcPrivacy }}
</Pill>
</template>
<template #items>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" />
</template>
</Popover>
</template>
{{ bcPrivacy }}
Items
Popovers contain a list of menu items. Items can contain different information based on their type.
::: info
Lists of items must be nested inside a <template #items>
tag directly under the <Popover>
tag.
:::
Popover item
The popover item (PopoverItem
) is a simple button that uses Vue event handling. Each item contains a slot which you can use to add a menu label and icon.
<script setup lang="ts">
const alert = (message: string) => window.alert(message)
const open = ref(false)
</script>
<template>
<Popover v-model="open">
<OptionsButton @click="open = !open" />
<template #items>
<PopoverItem @click="alert('Report this object?')">
<i class="bi bi-exclamation" />
Report
</PopoverItem>
</template>
</Popover>
</template>
Report
Checkbox
The checkbox (PopoverCheckbox
) is an item that acts as a selectable box. Use v-model
to bind the checkbox to a boolean value. Each checkbox contains a slot which you can use to add a menu label.
<script setup lang="ts">
const bc = ref(false)
const cc = ref(false)
const open = ref(false)
</script>
<template>
<Popover v-model="open">
<OptionsButton @click="open = !open" />
<template #items>
<PopoverCheckbox v-model="bc">
Bandcamp
</PopoverCheckbox>
<PopoverCheckbox v-model="cc">
Creative commons
</PopoverCheckbox>
</template>
</Popover>
</template>
Bandcamp
Creative commons
Radio
The radio (PopoverRadio
) is an item that acts as a radio selector.
Prop | Data type | Required? | Description |
---|---|---|---|
modelValue |
String | Yes | The current value of the radio. Use v-model to bind this to a value. |
choices |
Array<String> | Yes | A list of choices. |
<script setup lang="ts">
const open = ref(false);
const currentChoice = ref("pod");
const privacy = ["public", "private", "pod"];
</script>
<template>
<Popover v-model="open">
<OptionsButton @click="open = !open" />
<template #items>
<PopoverRadio v-model="currentChoice" :choices="choices" />
</template>
</Popover>
</template>
Separator
Use a standard horizontal rule (<hr>
) to add visual separators to popover lists.
<script setup lang="ts">
const bc = ref(false)
const cc = ref(false)
const open = ref(false)
</script>
<template>
<Popover v-model="open">
<OptionsButton @click="open = !open" />
<template #items>
<PopoverCheckbox v-model="bc">
Bandcamp
</PopoverCheckbox>
<hr>
<PopoverCheckbox v-model="cc">
Creative commons
</PopoverCheckbox>
</template>
</Popover>
</template>
Bandcamp
Creative commons
Icon Prop
PopoverItem supports an icon
prop to easily add icons to menu items. The icon prop accepts standard Bootstrap icon classes.
Prop | Data type | Required? | Description |
---|---|---|---|
icon |
String | No | Bootstrap icon class to display before the item |
<PopoverItem icon="bi-music-note-list">
Play next
</PopoverItem>
<PopoverItem icon="right bi-share">
Share
</PopoverItem>
Play next
Share
```
Submenus
To create more complex menus, you can use submenus (PopoverSubmenu
). Submenus are menu items which contain other menu items.
<script setup lang="ts">
const bc = ref(false)
const isOpen = ref(false)
</script>
<template>
<Popover v-model="isOpen">
<OptionsButton @click="isOpen = !isOpen" />
<template #items>
<PopoverSubmenu>
<i class="bi bi-collection" />
Organize and share
<template #items>
<PopoverCheckbox v-model="bc">
Bandcamp
</PopoverCheckbox>
</template>
</PopoverSubmenu>
</template>
</Popover>
</template>
Organize and share
Bandcamp
Extra items
You can add extra items to the right hand side of a popover item by nesting them in a <template #after>
tag. Use this to add additional menus or buttons to menu items.
<script setup lang="ts">
const bc = ref(false)
const privacyChoices = ['public', 'private', 'pod']
const bcPrivacy = ref('pod')
const isOpen = ref(false)
</script>
<template>
<Popover v-model="isOpen">
<OptionsButton @click="isOpen = !isOpen" />
<template #items>
<PopoverSubmenu>
<i class="bi bi-collection" />
Organize and share
<template #items>
<PopoverCheckbox v-model="bc">
Bandcamp
<template #after>
<Popover>
<template #default="{ toggleOpen }">
<Pill @click.stop="toggleOpen" :blue="bcPrivacy === 'pod'" :red="bcPrivacy === 'public'">
{{ bcPrivacy }}
</Pill>
</template>
<template #items>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices"/>
</template>
</Popover>
</template>
</PopoverCheckbox>
<hr>
<PopoverCheckbox v-model="share">
Share by link
<template #after>
<Button ghost @click.stop="alert('Link copied to clipboard')" round icon="bi-link" />
<Button ghost @click.stop="alert('Here is your code')" round icon="bi-code" />
</template>
</PopoverCheckbox>
</template>
</PopoverSubmenu>
</template>
</Popover>
</template>
Organize and share
Bandcamp
{{ bcPrivacy }}
Share by link
Links
You can use PopoverItem
s as Links by providing a to
prop with the route object or and external Url (http...
). Read more on the Link component page.
Menu
Here is an example of a completed menu containing all supported features.
<script setup lang="ts">
const isOpen = ref(false);
const bc = ref(false);
const cc = ref(false);
const share = ref(false);
const bcPrivacy = ref("pod");
const ccPrivacy = ref("public");
const privacyChoices = ["private", "pod", "public"];
</script>
<template>
<Popover v-model="isOpen">
<OptionsButton @click="isOpen = !isOpen" />
<template #items>
<PopoverSubmenu>
<i class="bi bi-music-note-list" />
Change language
<template #items>
<PopoverItem
v-for="(language, key) in SUPPORTED_LOCALES"
:key="key"
@click="setI18nLanguage(key)"
>
{{ language }}
</PopoverItem>
</template>
</PopoverSubmenu>
<PopoverItem>
<i class="bi bi-arrow-up-right" />
Play next
</PopoverItem>
<PopoverItem>
<i class="bi bi-arrow-down-right" />
Append to queue
</PopoverItem>
<PopoverSubmenu>
<i class="bi bi-music-note-list" />
Add to playlist
<template #items>
<PopoverItem>
<i class="bi bi-music-note-list" />
Sample playlist
</PopoverItem>
<hr />
<PopoverItem>
<i class="bi bi-plus-lg" />
New playlist
</PopoverItem>
</template>
</PopoverSubmenu>
<hr />
<PopoverItem>
<i class="bi bi-heart" />
Add to favorites
</PopoverItem>
<PopoverSubmenu>
<i class="bi bi-collection" />
Organize and share
<template #items>
<PopoverCheckbox v-model="bc">
Bandcamp
<template #after>
<Popover>
<template #default="{ toggleOpen }">
<Pill
@click.stop="toggleOpen"
:blue="bcPrivacy === 'pod'"
:red="bcPrivacy === 'public'"
>
{{ bcPrivacy }}
</Pill>
</template>
<template #items>
<PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" />
</template>
</Popover>
</template>
</PopoverCheckbox>
<PopoverCheckbox v-model="cc">
Creative Commons
<template #after>
<Popover v-model="isOpen">
<template #default="{ toggleOpen }">
<Pill
@click="toggleOpen"
:blue="ccPrivacy === 'pod'"
:red="ccPrivacy === 'public'"
>
{{ ccPrivacy }}
</Pill>
</template>
<template #items>
<PopoverRadio v-model="ccPrivacy" :choices="privacyChoices" />
</template>
</Popover>
</template>
</PopoverCheckbox>
<hr />
<PopoverItem>
<i class="bi bi-plus-lg" />
New library
</PopoverItem>
<hr />
<PopoverCheckbox v-model="share">
Share by link
<template #after>
<Button @click.stop ghost round icon="bi-link" />
<Button @click.stop ghost round icon="bi-code" />
</template>
</PopoverCheckbox>
</template>
</PopoverSubmenu>
<PopoverItem>
<i class="bi bi-cloud-download" />
Download
</PopoverItem>
<hr />
<PopoverItem>
<i class="bi bi-exclamation" />
Report
</PopoverItem>
</template>
</Popover>
</template>
Play next
Append to queue
Change language
{{ language }}
Add to playlist
Sample playlist
New playlist
Add to favorites Organize and share Bandcamp {{ bcPrivacy }} Creative Commons {{ ccPrivacy }}
New library
Share by link Download
Report