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
Space
andEnter
keys 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
Esc
key
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-vue
Dropdown 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-vue
Dropdown
styles. -
placement="bottom-start"
- Default placement of the dropdown -
auto-hide @show="onShow"
- Method that runs when theDropdown
gets shown -
@hide="onHide"
- Method that runs when theDropdown
gets hidden
-
The
itemSelector
prop - This is the selector that the component will use internally to get the dropdown items that keyboard navigation will cycle through.The
popoverKeydown
method - This method, that gets registered as adocument
keydown
listener when the dropdown gets shown (inonShow
method), includes all the keyboard functionality. We' re listening for up/down arrows key-presses and we cycle through anArray
of dropdown items settingfocus
on an item depending on the direction. We also listen forEnter
andSpace
key-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
onHide
method removes thedocument
keydown
listener 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!