Appearance
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: 8creates a visual gapmiddleware.flip: trueswitches sides when the preferred side does not fitmiddleware.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:
offsetfliporautoPlacementshiftsizearrow
If you want more detail, read Middleware Pipeline and Middleware Ordering Gotchas.
Next Step
- Read Middleware Pipeline if you want the deeper mental model.
- Read Middleware Ordering Gotchas if a stack is behaving strangely.
- Read Build Popovers and Dropdowns to see middleware inside a full click-driven surface.