Keep in View
Base placement gets you close. Real floating surfaces still run into viewport edges, scroll containers, and size limits. That is where middlewares 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.
The standard baseline stack
This is the stack many production surfaces end up using first.
<script setup lang="ts">
import { ref } from "vue";
import { useFloatingNode, usePosition } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
const open = ref(true);
const node = useFloatingNode({
anchorEl,
floatingEl,
open,
});
usePosition(node, {
placement: "bottom-start",
middlewares: {
offset: 8,
flip: true,
shift: { padding: 8 },
},
});
</script>
Read this stack from left to right:
middlewares.offset: 8creates a visual gapmiddlewares.flip: trueswitches sides when the preferred side does not fitmiddlewares.shift: { padding: 8 }nudges the panel back into view when needed
Add space from the trigger
Use offset to create space between the anchor and the floating panel:
middlewares: {
offset: 8,
}
Flip when the preferred side runs out of room
Use flip to try opposite or fallback placements when the preferred side runs out of room:
middlewares: {
offset: 8,
flip: true,
}
Shift to stay inside viewport boundaries
Use shift to slide the floating panel along its cross-axis so it stays within the visible viewport:
middlewares: {
offset: 8,
flip: true,
shift: { padding: 8 },
}
Match anchor width or constrain height
Use matchWidth to automatically lock the panel's width to the trigger's width (common for select dropdowns and comboboxes), or size for custom dimension constraints:
<script setup lang="ts">
import { ref } from "vue";
import { useFloatingNode, usePosition } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
const open = ref(true);
const node = useFloatingNode({
anchorEl,
floatingEl,
open,
});
usePosition(node, {
placement: "bottom-start",
middlewares: {
offset: 8,
flip: true,
shift: { padding: 8 },
matchWidth: true, // matches anchor element width
size: {
apply({ availableHeight }) {
if (!floatingEl.value) return;
Object.assign(floatingEl.value.style, {
maxHeight: `${availableHeight - 16}px`,
});
},
},
},
});
</script>
Pick the roomiest side with auto-placement
Use autoPlacement when you want VFloat to inspect available space across all sides and pick the roomiest side dynamically:
middlewares: {
offset: 8,
autoPlacement: true,
shift: { padding: 8 },
}
Note that autoPlacement and flip are mutually exclusive strategies. Choose flip when you have a preferred side with fallbacks, or autoPlacement when any roomy side is acceptable.
Point an arrow back at the anchor
Use useArrow to position a pointed indicator element and compute its alignment styles:
<script setup lang="ts">
import { ref } from "vue";
import { useArrow, useFloatingNode, 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 node = useFloatingNode({
anchorEl,
floatingEl,
arrowEl,
open,
});
usePosition(node, {
middlewares: {
offset: 8,
flip: true,
shift: { padding: 8 },
},
});
// Automatically registers arrow middleware and synchronizes styles to arrowEl
useArrow(node);
</script>
useArrow(node) registers the arrow middleware into the positioning registry and automatically applies the computed physical inset styles (top, bottom, left, right) directly to node.refs.arrowEl.
Middleware order matters
Order is not a cosmetic detail. Middlewares run sequentially, each transforming the coordinates produced by earlier steps.
When using declarative middlewares options, VFloat executes them in canonical order:
inline. Resolves multi-line anchor geometry.offset. Establishes the gap before collision checks.fliporautoPlacement. Selects the best viable placement.shift. Nudges the surface inside viewport boundaries.matchWidthorsize. Applies width and height constraints.hide. Computes visibility when anchor is clipped.arrow. Centers the arrow against the final surface position.custom. Executes user-provided raw middleware functions.
If you pass a raw array to middlewares (middlewares: [offset(8), flip(), shift()]), you control the exact sequence yourself.
Where to go next
- Read Middleware Pipeline for the architectural mental model.
- Read Middleware Ordering Gotchas to diagnose subtle collision bugs.
- Read Build Popovers and Dropdowns to see middleware inside a real component.