DEV Community 👩‍💻👨‍💻

Cover image for A Vue dropdown menu with Floating Vue & Tailwind
Giannis Koutsaftakis
Giannis Koutsaftakis

Posted on

A Vue dropdown menu with Floating Vue & Tailwind

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.

Image description

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 and Enter 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
  • Set some component defaults

    • ref="popoverRef" - The popover ref, 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 default floating-vue Dropdown styles.
    • placement="bottom-start" - Default placement of the dropdown
    • auto-hide @show="onShow" - Method that runs when the Dropdown gets shown
    • @hide="onHide" - Method that runs when the Dropdown 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 a document keydown listener when the dropdown gets shown (in onShow method), includes all the keyboard functionality. We' re listening for up/down arrows key-presses and we cycle through an Array of dropdown items setting focus on an item depending on the direction. We also listen for Enter and Space key-presses so when these take place on the dropdown trigger, we open the dropdown. Notice the usage of e.preventDefault();, this is so that the page doesn't scroll while using up/down keys when the dropdown is open.

  • The onHide method removes the document 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>
Enter fullscreen mode Exit fullscreen mode

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 (0)

🤯

"I made 10x faster JSON.stringify() functions, even type safe"

☝️ Must read for JS devs