Floatit Usage Guide
New to Floatit?
Start with the Overview for a quick introduction, then come back here for in-depth patterns.
Placement
A Placement is a Side optionally combined with an Alignment:
Side: 'top' | 'bottom' | 'left' | 'right'
Alignment: 'start' | 'end'
Examples: 'top' | 'top-start' | 'top-end'
'bottom' | 'bottom-start' | 'bottom-end'
'left' | 'left-start' | 'left-end'
'right' | 'right-start' | 'right-end'All positions use getBoundingClientRect coordinates, meaning they work correctly with position: fixed floating elements.
positionFloat
positionFloat computes the position and immediately applies left / top inline styles to the floating element, returning the resolved Placement (which may differ from the requested one if flip was applied).
const placement = positionFloat(reference, floating, {
placement: 'top',
middleware: [offset(8), flip(), shift({ padding: 6 })],
});
floatingEl.dataset.placement = placement; // react to flipsThe floating element must have position: fixed in your CSS for the applied left/top values to take effect.
computePosition
The low-level API. Returns { x, y, placement } without touching the DOM — useful when you want to apply styles yourself or integrate with animation libraries.
const { x, y, placement } = computePosition(reference, floating, {
placement: 'bottom-start',
middleware: [flip(), shift({ padding: 4 })],
});
floating.style.transform = `translate(${x}px, ${y}px)`;Middleware
Middleware are small functions that modify the computed position. They run in order, each receiving and returning a MiddlewareState.
offset
Adds a gap (in pixels) between the reference and the floating element along the main axis.
// 8px gap between reference and floating
middleware: [offset(8)];flip
Flips the floating element to the opposite side when it would overflow the viewport.
// Default: flip when any part overflows
middleware: [flip()];
// Keep 8px clearance before flipping
middleware: [flip({ padding: 8 })];When flip changes the placement, computePosition restarts the middleware pipeline using the new placement so all subsequent middlewares (e.g. shift) receive the correct coordinates.
shift
Slides the floating element along its cross axis so it stays within the viewport.
// Keep at least 6px from every viewport edge
middleware: [shift({ padding: 6 })];size
Calls an apply callback with { availableWidth, availableHeight, elements }. Use it to constrain or resize the floating element.
middleware: [
size({
padding: 8,
apply({ availableWidth, elements }) {
// Match dropdown width to trigger
elements.floating.style.width = `${elements.reference.getBoundingClientRect().width}px`;
// or cap the max height
elements.floating.style.maxHeight = `${availableHeight}px`;
},
}),
];Middleware order matters
Middlewares run sequentially. The recommended order is:
middleware: [
offset(8), // 1. push away from reference first
flip(), // 2. flip side if needed (triggers a pipeline restart)
shift({ padding: 6 }), // 3. nudge into viewport after final side is known
size({ apply: ({ availableHeight }) => console.log(availableHeight) }), // 4. resize with final available space
];Custom middleware
You can write your own by returning a modified MiddlewareState:
import type { Middleware } from '@vielzeug/floatit';
const nudge = (px: number): Middleware => ({
name: 'nudge',
fn: (state) => ({ ...state, y: state.y + px }),
});
middleware: [offset(8), nudge(4), flip()];autoUpdate
float
float is the primary API for dynamic positioning. It positions the floating element immediately and keeps it in sync as the viewport or elements change. It combines positionFloat and autoUpdate in a single call.
let cleanup: (() => void) | null = null;
function show() {
cleanup = float(reference, floating, {
placement: 'top',
middleware: [offset(8), flip(), shift({ padding: 6 })],
});
}
function hide() {
cleanup?.();
cleanup = null;
}autoUpdate
autoUpdate is the lower-level primitive behind float. Use it directly when you need to run custom logic (e.g. reading placement to update an arrow) on every reposition.
const cleanup = autoUpdate(reference, floating, () => {
const placement = positionFloat(reference, floating, {
placement: 'top',
middleware: [offset(8), flip(), shift({ padding: 6 })],
});
floating.dataset.placement = placement;
});autoUpdate calls the callback once immediately on registration, then listens to:
scrollonwindow(capturing, covers all scroll ancestors)resizeonwindowResizeObserveron the reference and (by default) the floating elementwindow.visualViewport(by default, covers pinch-zoom and virtual keyboard)
If your floating element's size is fully controlled externally and observing it causes unnecessary update loops, disable floating observation:
const cleanup = autoUpdate(reference, floating, update, {
observeFloating: false,
});Always call the returned cleanup when the floating element is hidden to avoid unnecessary reflows.
Common Patterns
Tooltip
import { float, flip, offset, shift } from '@vielzeug/floatit';
let cleanup: (() => void) | null = null;
function showTooltip(trigger: Element, tooltip: HTMLElement) {
tooltip.showPopover?.() ?? tooltip.setAttribute('data-open', '');
cleanup = float(trigger, tooltip, {
placement: 'top',
middleware: [offset(8), flip(), shift({ padding: 6 })],
});
}
function hideTooltip(tooltip: HTMLElement) {
tooltip.hidePopover?.() ?? tooltip.removeAttribute('data-open');
cleanup?.();
cleanup = null;
}Dropdown / Select
import { float, flip, shift, size } from '@vielzeug/floatit';
function openDropdown(trigger: HTMLElement, panel: HTMLElement) {
return float(trigger, panel, {
placement: 'bottom-start',
middleware: [
flip({ padding: 6 }),
shift({ padding: 6 }),
size({
padding: 6,
apply({ elements }) {
elements.floating.style.width = `${(elements.reference as HTMLElement).offsetWidth}px`;
},
}),
],
});
}