One of the most popular libraries available for creating tooltips, popovers and menus in Vue applications is floating-vue. Although floating-vue provides a Dropdown component, styling it and adding additional functionality, like e.g keyboard navigation, is left up to the user.
What we'll create
We'll use the Dropdown component of floating-vue as a base and then add keyboard functionality on top. We'll then use that component to create a dropdown menu using Tailwind.
The end result will resemble Bootstrap's Dropdown component.
Let's get to it then!
Dropdown component
Luckily Vue.js allows us to easily extend any component using the "wrapper component" technique. So our final component will include all functionality of floating-vue Dropdown plus:
- Capability to move through individual dropdown items using the cursor up/down keys
- Customizable CSS selector for the dropdown items used for the keyboard navigation
- Open and close the dropdown with the
SpaceandEnterkeys when the dropdown trigger has focus - Prevent page scroll while navigating the menu with the keyboard
- Close the dropdown by clicking outside or by using the
Esckey
Dropdown.vue
<template>
<FloatingVueDropdown ref="popoverRef" :distance="14" :triggers="['click']" theme="dropdown-menu" placement="bottom-start" auto-hide @show="onShow" @hide="onHide">
<template v-for="(_, slot) in $slots" #[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
</FloatingVueDropdown>
</template>
<script setup>
import { Dropdown as FloatingVueDropdown } from 'floating-vue';
import { onBeforeUnmount, ref } from 'vue';
const popoverRef = ref(null);
const props = defineProps({
itemSelector: {
type: String,
default: 'li > a:not(:disabled)',
},
});
const popoverKeydown = (e) => {
const popover = popoverRef.value;
if (!popover) {
return;
}
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
const items = [...popover.$refs.popperContent.$el.querySelectorAll(props.itemSelector)];
if (!items.length) {
return;
}
let index = items.indexOf(e.target);
index = e.key === 'ArrowDown' ? index + 1 : index - 1;
items[index]?.focus();
}
if ((e.key === 'Enter' || e.key === ' ') && e.target === popover.$refs.popperContent.$el) {
e.preventDefault();
popover.hide();
}
};
const onShow = () => {
document.addEventListener('keydown', popoverKeydown);
};
const onHide = () => {
document.removeEventListener('keydown', popoverKeydown);
const popover = popoverRef.value;
popover?.$refs.popper.$_targetNodes[0].focus();
};
onBeforeUnmount(() => {
document.removeEventListener('keydown', popoverKeydown);
});
</script>
<style>
.v-popper--theme-dropdown-menu .v-popper__arrow-container {
display: none;
}
</style>
Breakdown of the most important parts
- Pass down slots to the
floating-vueDropdown component.
<FloatingVueDropdown>
<template v-for="(_, slot) in $slots" #[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
</FloatingVueDropdown>
-
Set some component defaults
-
ref="popoverRef"- The popoverref, we need it for our DOM related operations. -
:distance="14"- Set the distance between the dropdown trigger and the dropdown menu -
:triggers="['click']"- Open the dropdown on click -
theme="dropdown-menu"- Set the a theme to our own theme, essentially removing all the defaultfloating-vueDropdownstyles. -
placement="bottom-start"- Default placement of the dropdown -
auto-hide @show="onShow"- Method that runs when theDropdowngets shown -
@hide="onHide"- Method that runs when theDropdowngets hidden
-
The
itemSelectorprop - This is the selector that the component will use internally to get the dropdown items that keyboard navigation will cycle through.The
popoverKeydownmethod - This method, that gets registered as adocumentkeydownlistener when the dropdown gets shown (inonShowmethod), includes all the keyboard functionality. We' re listening for up/down arrows key-presses and we cycle through anArrayof dropdown items settingfocuson an item depending on the direction. We also listen forEnterandSpacekey-presses so when these take place on the dropdown trigger, we open the dropdown. Notice the usage ofe.preventDefault();, this is so that the page doesn't scroll while using up/down keys when the dropdown is open.The
onHidemethod removes thedocumentkeydownlistener and returns focus to the dropdown trigger.
Structure and Styling
We have our base Dropdown component ready, let's use it to create a dropdown menu using Tailwind!
App.vue
<template>
<Dropdown class="inline" popper-class="w-64 bg-white border rounded-lg shadow-md">
<!-- Dropdown trigger -->
<button
class="inline-block px-6 py-3 bg-blue-600 text-white leading-tight rounded hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
>
Dropdown button
</button>
<!-- Dropdown content -->
<template #popper="{ hide }">
<ul class="py-1 text-gray-70">
<li>
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none" @click="hide">
Click me to close
</a>
</li>
<li>
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none">Menu item</a>
</li>
<li>
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none">Another menu item</a>
</li>
</ul>
</template>
</Dropdown>
</template>
<script setup>
import Dropdown from './Dropdown.vue'
</script>
That's it!
Here is a live example of the end result in this Stackblitz here.
You might also want to check out vue-simple-dropdown, a dropdown component for floating-vue ready to be imported into your Vue project!

Top comments (1)
You saved my life. Thank you!