Skip to content

Keep Content in View

Base placement gets you close. Real floating surfaces still run into viewport edges, scroll containers, and size limits. That is where middleware options help.

This guide treats middleware like a small set of fixes. When you notice a problem, pick the fix that matches it.

Start From The Problem

The easiest way to reason about middleware is to ask:

  • Does the surface need space from the anchor?
  • Does it need to change sides when space runs out?
  • Does it need to stay inside visible boundaries?
  • Does it need to resize itself?
  • Does it need an arrow?

Each of those problems maps to a middleware or helper.

Start With The Most Common Stack

This is the stack many production surfaces end up using first.

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 open = ref(true);

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

Read this stack from left to right:

  • middleware.offset: 8 creates a visual gap
  • middleware.flip: true switches sides when the preferred side does not fit
  • middleware.shift: { padding: 8 } nudges the panel back into view when needed

Problem 1: "The Surface Feels Jammed Against The Trigger"

Use offset.

Problem 2: "The Preferred Side Does Not Fit"

Use flip.

Problem 3: "It Still Overflows Even After Flipping"

Use shift.

Problem 4: "The Panel Should Match Width Or Fit Height"

Use size.

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

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

const context = useFloatingContext({
  refs: { anchorEl, floatingEl },
  state: { open },
});
const { styles } = usePosition(context, {
  placement: "bottom-start",
  middleware: {
    offset: 8,
    flip: true,
    shift: { padding: 8 },
    custom: [
      size({
        apply({ rects, availableHeight }) {
          if (!floatingEl.value) return;

          Object.assign(floatingEl.value.style, {
            minWidth: `${rects.reference.width}px`,
            maxHeight: `${availableHeight - 16}px`,
          });
        },
      }),
    ],
  },
});
</script>

Problem 5: "I Want The Best Side Automatically"

Use autoPlacement when the exact side is less important than finding the side with the most room.

Problem 6: "I Need An Arrow"

Use useArrow to register the arrow middleware and read arrow styles.

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

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

const context = useFloatingContext({
  refs: { anchorEl, floatingEl, arrowEl },
  state: { open },
});
const position = usePosition(context, {
  middleware: {
    offset: 8,
  },
});
const { styles } = position;

const { arrowStyles } = useArrow(context, position);
</script>

Middleware Order Matters

Order is not a formatting detail. It changes outcomes.

A common baseline order is:

  1. offset
  2. flip or autoPlacement
  3. shift
  4. size
  5. arrow

If you want more detail, read Middleware Pipeline and Middleware Ordering Gotchas.

Next Step

Keep Content in View has loaded