Skip to content

Build Popovers and Dropdowns

Let's build the next common surface: a popover or dropdown.

Tooltips teach hover. Popovers usually teach click. The shape is still simple, but the rules around opening, closing, and focus are a little richer.

For a straightforward popover or dropdown, start with:

Step 1: Create The Positioning Context

Start with the anchor, the floating element, and a shared context.

vue
<script setup lang="ts">
import { ref } from "vue";
import { useFloatingContext, usePosition } from "v-float";

const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const context = useFloatingContext({ refs: { anchorEl, floatingEl } });
const { styles } = usePosition(context, {
  placement: "bottom-start",
  middleware: {
    offset: 8,
  },
});
</script>

Step 2: Add Open And Dismissal Behavior

Now add the behavior that makes the surface feel like a popover.

vue
<script setup lang="ts">
import { ref } from "vue";
import { useClick, useEscapeKey, useFloatingContext, useOutsideClick, usePosition } from "v-float";

const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const context = useFloatingContext({ refs: { anchorEl, floatingEl } });
const { styles } = usePosition(context, {
  placement: "bottom-start",
  middleware: {
    offset: 8,
  },
});

useClick(context);
useOutsideClick(context);
useEscapeKey(context);
</script>

That gives you a good default:

  • Anchor click toggles the panel
  • Outside pointer interaction closes it
  • Escape closes it

Step 3: Render The Panel

Now render the content the user actually came for.

vue
<template>
  <button ref="anchorEl" type="button" class="trigger">Open actions</button>

  <div v-if="context.state.open.value" ref="floatingEl" class="panel" :style="styles">
    <h2>Quick actions</h2>
    <p>Choose the next step for this record.</p>
    <div class="actions">
      <button type="button">Edit</button>
      <button type="button">Duplicate</button>
      <button type="button">Archive</button>
    </div>
  </div>
</template>

A Safer Default For Real Panels

If the panel can run into the viewport edge, set middleware.flip: true and middleware.shift.

vue
<script setup lang="ts">
import { ref } from "vue";
import { useClick, useEscapeKey, useFloatingContext, useOutsideClick, usePosition } from "v-float";

const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const context = useFloatingContext({ refs: { anchorEl, floatingEl } });
const { styles } = usePosition(context, {
  placement: "bottom-start",
  middleware: {
    offset: 8,
    flip: true,
    shift: { padding: 8 },
  },
});

useClick(context);
useOutsideClick(context);
useEscapeKey(context);
</script>

That stack is common for real dropdown-like UI because it handles spacing, fallback sides, and boundary safety in one pass.

Where To Go Next

Build Popovers and Dropdowns has loaded