A Vue dropdown menu with Floating Vue & Tailwind
Tags: vue, javascript, webdev, ux
Published: 2022-08-28 20:40:23

One of the most popular libraries available for creating tooltips, popovers and menus in Vue applications is [floating-vue](https://floating-vue.starpad.dev/). Although `floating-vue` provides a [Dropdown](https://floating-vue.starpad.dev/guide/component.html#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](https://getbootstrap.com/docs/5.2/components/dropdowns/#single-button).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bhfvf92rh4e09gyuw610.png)

**Let's get to it then!**

## Dropdown component

Luckily Vue.js allows us to easily extend any component using the "[wrapper component](https://dev.to/kouts/create-wrapper-components-for-vuetify-components-2aah)" 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`

```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. 
```vue
<FloatingVueDropdown>
  <template v-for="(_, slot) in $slots" #[slot]="scope">
    <slot :name="slot" v-bind="scope || {}" />
  </template>
</FloatingVueDropdown>
```

- 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`

```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](https://stackblitz.com/edit/vue3-tailwind-jehvgf?file=src/App.vue).

You might also want to check out [vue-simple-dropdown](https://github.com/kouts/vue-simple-dropdown), a dropdown component for `floating-vue` ready to be imported into your Vue project!