TIL

Floating UI - Middleware Overview

文章發表於
...

In the , we explored how computeCoordsFromPlacement calculates the initial x and y coordinates. But that's just the starting point. The real power of Floating UI lies in its middleware pipeline - a sequential, resettable, data-sharing loop that transforms positioning through composable functions.

Overview

Part 1: The Pipeline Architecture

The middleware pipeline is the heart of Floating UI's extensibility. It transforms positioning through composable functions that run in sequence.

export interface MiddlewareData {
[key: string]: any;
arrow?: Partial<Coords> & {
centerOffset: number;
alignmentOffset?: number;
};
autoPlacement?: {
index?: number;
overflows: Array<{
placement: Placement;
overflows: Array<number>;
}>;
};
flip?: {
index?: number;
overflows: Array<{
placement: Placement;
overflows: Array<number>;
}>;
};
hide?: {
referenceHidden?: boolean;
escaped?: boolean;
referenceHiddenOffsets?: SideObject;
escapedOffsets?: SideObject;
};
offset?: Coords & {placement: Placement};
shift?: Coords & {
enabled: {[key in Axis]: boolean};
};
}

Step 1. Initial positioning

let { x, y } = computeCoordsFromPlacement(rects, placement)

The process begins with an initial position. The computeCoordsFromPlacement looks at the reference element and the requested placement to calculate a basic starting point.

Step 2. The Middleware Loop

The initial coordinates are passed through a sequence of middleware functions. Each middleware has a single responsibility (e.g. offset adds a gap, flip handles viewport collisions).

for (let i = 0; i < validMiddleware.length; i++) {
const {name, fn} = validMiddleware[i];
// ... call fn, process result
if (reset && resetCount <= 50) {
// ... handle reset
i = -1; // After i++, becomes 0 - restart from first middleware
}
}
Normal flow: i=0 → process → i++ → i=1 → process → i++ → done
Reset at i=1: i=0 → process → i++ → i=1 → RESET → i=-1 → i++ → i=0 → restart

Step 3. Building the Middleware State

Each middleware receives a comprehensive state object and returns modifications:

const {
x: nextX,
y: nextY,
data,
reset,
} = await fn({
x, // Current x position
y, // Current y position
initialPlacement: placement, // Original placement (never changes)
placement: statefulPlacement, // Current placement (may have changed)
strategy, // 'absolute' | 'fixed'
middlewareData, // Data from previous middleware
rects, // Element rectangles
platform, // Platform methods
elements: {reference, floating}, // Actual elements
});

Why both initialPlacement and placement? The initialPlacement preserves the user's original intent (e.g., 'bottom'), while placement reflects the current working state (may become 'top' after flip). This lets flip know what to fall back to while offset applies the gap in the correct direction.

Step 4. Processing Middleware Return

After each middleware runs, we apply its modifications:

// Apply position changes (if any)
x = nextX ?? x;
y = nextY ?? y;
// Accumulate data under middleware's namespace
middlewareData = {
...middlewareData, // Keep existing data
[name]: { // Namespace by middleware name
...middlewareData[name], // Keep existing data for this middleware
...data, // Merge new data
},
};

For example, if we have offset and flip in our middleware, after executing it, the middleware data will look like this:

middlewareData = {
offset: { mainAxis: 10 },
flip: { index: 1, overflows: [...] }
}

Step 5. The Reset Mechanism

When a middleware needs to change placement (like flip), it triggers a reset:

if (reset && resetCount <= 50) {
resetCount++;
if (typeof reset === 'object') {
if (reset.placement) {
statefulPlacement = reset.placement;
}
if (reset.rects) {
rects =
reset.rects === true
? await platform.getElementRects({reference, floating, strategy})
: reset.rects;
}
({x, y} = computeCoordsFromPlacement(rects, statefulPlacement, rtl));
}
i = -1;
}

Reset types:

Return ValueEffect
{ reset: true }Restart loop with current state
{ reset: { placement: 'top' } }Change placement, recompute coords, restart
{ reset: { rects: true } }Re-measure elements, recompute coords, restart

Complete Example

Let's trace through a real scenario where a tooltip needs to flip:

computePosition(buttonEl, tooltipEl, {
placement: 'bottom',
middleware: [offset(30), flip()],
});

The Scenario

Imagine a button near the bottom of your screen at y = 500. It is 40px tall. We want to place an 80px tall tooltip below it, but the viewport is only 600px tall.

Initial Placement: With placement: 'bottom', the tooltip starts directly under the button at y = 540 (500 + 40).

{
x: 250, // Centered under reference
y: 540, // reference.y + reference.height = 500 + 40
initialPlacement: 'bottom', // User's original intent (never changes)
statefulPlacement: 'bottom', // Current working placement
middlewareData: {}, // Empty - no middleware has run yet
resetCount: 0
}

The Problem: The tooltip's bottom edge would be at 620 (540 + 80). Since the viewport ends at 600, we already have an overflow!

Pass 1, i=0: offset(30)

The middleware pipeline runs. The offset middleware's job is to push the floating element away from the reference.

Since we are at the bottom, it adds 30 px to the y-coordinate.

{
x: 250,
y: 570, // Updated: 540 + 30
initialPlacement: 'bottom',
statefulPlacement: 'bottom',
middlewareData: {
offset: { mainAxis: 30, crossAxis: 0, placement: 'bottom' }
},
resetCount: 0
}

Pass 1, i=1: flip()

The flip() middleware sees this overflow and effectively says, "There's no room down here!" It resets the placement to 'top' and recalculates everything to fit perfectly safely above the button.

{
x: 250,
y: 420, // Recomputed: reference.y - floating.height = 500 - 80
initialPlacement: 'bottom', // Still 'bottom' - preserves user intent
statefulPlacement: 'top', // Changed! flip triggered this
middlewareData: {
offset: { mainAxis: 30, crossAxis: 0, placement: 'bottom' },
flip: {
index: 0,
overflows: [{ placement: 'bottom', overflows: [50] }] // Recorded the overflow
}
},
resetCount: 1 // Incremented to prevent infinite loops
}
// Loop restarts: i = -1 → i++ → i = 0

Pass 2, i=0: offset(30) again

Now that placement is 'top', offset pushes the floating element upward (away from the reference).

// STATE AFTER offset(30) - Pass 2
{
x: 250,
y: 390, // Updated: 420 - 30 (negative direction for 'top')
initialPlacement: 'bottom',
statefulPlacement: 'top',
middlewareData: {
offset: { mainAxis: 30, crossAxis: 0, placement: 'top' }, // Updated with new placement
flip: { index: 0, overflows: [...] }
},
resetCount: 1
}

Pass 2, i=1: flip() again

{
x: 250,
y: 390, // Unchanged
initialPlacement: 'bottom',
statefulPlacement: 'top',
middlewareData: {
offset: { mainAxis: 30, crossAxis: 0, placement: 'top' },
flip: { index: 0, overflows: [...] } // Unchanged
},
resetCount: 1
}

Final Result:

// FINAL OUTPUT returned by computePosition()
{
x: 250, // Final x coordinate
y: 390, // Final y coordinate
placement: 'top', // Changed from 'bottom'!
middlewareData: {
offset: { mainAxis: 30, crossAxis: 0, placement: 'top' },
flip: {
index: 0,
overflows: [{ placement: 'bottom', overflows: [50] }]
}
}
}

Complete Flow Summary:

StepiMiddlewarexyplacementAction
Initial--250540bottomcomputeCoordsFromPlacement
Pass 10offset(30)250570bottomy += 30
Pass 11flip()250420topRESET! Overflow detected
Pass 20offset(30)250390topy -= 30
Pass 21flip()250390topNo overflow, done

Notice how offset runs twice - once for each placement direction. This is why the reset mechanism exists: when placement changes, all middleware must reprocess with the new context. The initialPlacement ('bottom') is preserved so middleware can always reference the user's original intent, while placement reflects the actual working state.

Summary

The Floating UI middleware pipeline is a powerful pattern that transforms simple coordinate calculations into a flexible, extensible positioning system. Here are the key takeaways:

The Pipeline Pattern

  • Middleware functions run sequentially in a for loop, each receiving the current state and returning modifications
  • Each middleware has a single responsibility: offset adds gaps, flip handles collisions, shift keeps elements in view
  • Data accumulates in middlewareData under each middleware's namespace, allowing later middleware to access earlier results

The Reset Mechanism

  • When a middleware returns { reset: true } or { reset: { placement: '...' } }, the loop restarts from i = 0
  • This allows middleware like flip to change placement and have all previous middleware (like offset) reprocess with the new context
  • A resetCount limit (50) prevents infinite loops from misconfigured middleware

State Management

  • initialPlacement: The user's original intent - never changes during processing
  • placement (statefulPlacement): The current working placement - may change via reset
  • middlewareData: Accumulated data from all middleware runs, namespaced by middleware name

Why This Design Works

  • Composable: Add or remove middleware without changing others
  • Order-independent results: The reset mechanism ensures correct final positioning regardless of when collisions are detected
  • Debuggable: The middlewareData object provides full visibility into what each middleware contributed

This architecture enables Floating UI to handle complex positioning scenarios - from simple tooltips to elaborate dropdown menus with arrows, boundaries, and dynamic placement - all through the same unified pipeline.

If you enjoyed this article, please click the buttons below to share it with more people. Your support means a lot to me as a writer.
Share on X
Buy me a coffee