Middleware
Objects that change the positioning of the floating element, executed in order as a stack.
Middleware allow you to customize the behavior of the positioning and be as granular as you want, adding your own custom logic.
computePosition()
computePosition()
starts with initial positioning via
placement
placement
— then middleware are executed as an
in-between “middle” step of the initial placement computation and
eventual return of data for rendering.
Each middleware is executed in order:
computePosition(referenceEl, floatingEl, {
placement: 'right',
middleware: [],
});
computePosition(referenceEl, floatingEl, {
placement: 'right',
middleware: [],
});
Example
const shiftByOnePixel = {
name: 'shiftByOnePixel',
fn({x, y}) {
return {
x: x + 1,
y: y + 1,
};
},
};
const shiftByOnePixel = {
name: 'shiftByOnePixel',
fn({x, y}) {
return {
x: x + 1,
y: y + 1,
};
},
};
This (not particularly useful) middleware adds 1
1
pixel to
the coordinates. To use this middleware, add it to your
middleware
middleware
array:
computePosition(referenceEl, floatingEl, {
placement: 'right',
middleware: [shiftByOnePixel],
});
computePosition(referenceEl, floatingEl, {
placement: 'right',
middleware: [shiftByOnePixel],
});
Here, computePosition()
computePosition()
will compute coordinates that will
place the floating element to the right
right
center of the
reference element, lying flush with it.
Middleware are then executed, resulting in these coordinates getting shifted by one pixel. Then that data is returned for rendering.
Shape
A middleware is an object that has a name
name
property
and a fn
fn
property. The fn
fn
property
provides the logic of the middleware, which returns new
positioning coordinates or useful data.
Data
Any data can be passed via an optional data
data
property of the object that is returned from fn
fn
.
This will be accessible to the consumer via the middlewareData
property:
const shiftByOnePixel = {
name: 'shiftByOnePixel',
fn({x, y}) {
return {
x: x + 1,
y: y + 1,
data: {
amount: 1,
},
};
},
};
const shiftByOnePixel = {
name: 'shiftByOnePixel',
fn({x, y}) {
return {
x: x + 1,
y: y + 1,
data: {
amount: 1,
},
};
},
};
computePosition(referenceEl, floatingEl, {
middleware: [shiftByOnePixel],
}).then(({middlewareData}) => {
console.log(middlewareData.shiftByOnePixel);
});
computePosition(referenceEl, floatingEl, {
middleware: [shiftByOnePixel],
}).then(({middlewareData}) => {
console.log(middlewareData.shiftByOnePixel);
});
Function
You may notice that Floating UI’s packaged middleware are actually functions. This is so you can pass options in, changing how the middleware behaves:
const shiftByAmount = (amount = 0) => ({
name: 'shiftByAmount',
options: amount,
fn: ({x, y}) => ({
x: x + amount,
y: y + amount,
}),
});
const shiftByAmount = (amount = 0) => ({
name: 'shiftByAmount',
options: amount,
fn: ({x, y}) => ({
x: x + amount,
y: y + amount,
}),
});
It returns an object and uses a closure to pass the configured behavior:
const middleware = [shiftByAmount(10)];
const middleware = [shiftByAmount(10)];
The options
options
key on a middleware object holds the
dependencies, allowing deep comparison reactivity.
Always return an object
Inside fn
fn
make sure to return an object. It doesn’t
need to contain properties, but to remind you that it should be
pure, you must return an object. Never mutate any values that get
passed in from fn
fn
.
MiddlewareState
An object is passed to fn
fn
containing useful data
about the middleware lifecycle being executed.
In the previous examples, we destructured x
x
and
y
y
out of the fn
fn
parameter object. These
are only two properties that get passed into middleware, but
there are many more.
The properties passed are below:
interface MiddlewareState {
x: number;
y: number;
initialPlacement: Placement;
placement: Placement;
strategy: Strategy;
middlewareData: MiddlewareData;
elements: Elements;
rects: ElementRects;
platform: Platform;
}
interface MiddlewareState {
x: number;
y: number;
initialPlacement: Placement;
placement: Placement;
strategy: Strategy;
middlewareData: MiddlewareData;
elements: Elements;
rects: ElementRects;
platform: Platform;
}
x
This is the x-axis coordinate to position the floating element to.
y
This is the y-axis coordinate to position the floating element to.
elements
This is an object containing the reference and floating elements.
rects
This is an object containing the Rect
Rect
s of the
reference and floating elements, an object of shape
{width, height, x, y}
.
middlewareData
This is an object containing all the data of any middleware at
the current step in the lifecycle. The lifecycle loops over the
middleware
middleware
array, so later middleware have access
to data from any middleware run prior.
strategy
The positioning strategy.
initialPlacement
The initial (or preferred) placement passed in to
computePosition()
computePosition()
.
placement
The stateful resultant placement. Middleware like
flip
flip
change initialPlacement
initialPlacement
to a
new one.
platform
An object containing methods to make Floating UI work on the current platform, e.g. DOM or React Native.
Ordering
The order in which middleware are placed in the array matters, as middleware use the coordinates that were returned from previous ones. This means they perform their work based on the current positioning state.
Three shiftByOnePixel
shiftByOnePixel
in the middleware array means
the coordinates get shifted by 3 pixels in total:
const shiftByOnePixel = {
name: 'shiftByOnePixel',
fn: ({x, y}) => ({x: x + 1, y: y + 1}),
};
const middleware = [
shiftByOnePixel,
shiftByOnePixel,
shiftByOnePixel,
];
const shiftByOnePixel = {
name: 'shiftByOnePixel',
fn: ({x, y}) => ({x: x + 1, y: y + 1}),
};
const middleware = [
shiftByOnePixel,
shiftByOnePixel,
shiftByOnePixel,
];
If the later shiftByOnePixel
implementations had a condition
based on the current value of x
x
and y
y
, the
condition can change based on their placement in the array.
Understanding this can help in knowing which order to place middleware in, as placing a middleware before or after another can produce a different result.
In general, offset()
offset()
should always go at the beginning of
the middleware array, while arrow()
arrow()
and hide()
hide()
at
the end. The other core middleware can be shifted around
depending on the desired behavior.
const middleware = [
offset(),
// ...
arrow({element: arrowElement}),
hide(),
];
const middleware = [
offset(),
// ...
arrow({element: arrowElement}),
hide(),
];
Resetting the lifecycle
There are use cases for needing to reset the middleware lifecycle so that other middleware perform fresh logic.
- When
flip()
flip()
andautoPlacement()
autoPlacement()
change the placement, they reset the lifecycle so that other middleware that modify the coordinates based on the currentplacement
placement
do not perform stale logic. size()
size()
resets the lifecycle with the newly applied dimensions, as many middleware read the dimensions to perform their logic.inline()
inline()
resets the lifecycle when it changes the reference rect to a custom implementation, similar to a Virtual Element.
In order to do this, add a reset
reset
property to the
returned object from fn
fn
.
type Reset =
| true
| {
placement?: Placement;
// `true` will compute the new `rects` if the
// dimensions were mutated. Otherwise, you can
// return your own new rects.
rects?: true | ElementRects;
};
type Reset =
| true
| {
placement?: Placement;
// `true` will compute the new `rects` if the
// dimensions were mutated. Otherwise, you can
// return your own new rects.
rects?: true | ElementRects;
};
const middleware = {
name: 'middleware',
fn() {
if (someCondition) {
return {
reset: {
placement: nextPlacement,
},
};
}
return {};
},
};
const middleware = {
name: 'middleware',
fn() {
if (someCondition) {
return {
reset: {
placement: nextPlacement,
},
};
}
return {};
},
};
Data supplied to middlewareData
middlewareData
is preserved by doing
this, so you can read it at any point after you’ve reset the
lifecycle.