Introduction
Rust Floating UI is a Rust port of Floating UI.
Floating UI is a library that helps you create “floating” elements such as tooltips, popovers, dropdowns, and more.
It provides a toolkit of positioning features that let you robustly anchor an absolutely-positioned floating element next to a given reference element. For example, a popover floats next to and remains anchored to its triggering button, even while the page scrolls.
It also provides features to avoid collisions with the viewport, as absolute positioning often leads to unwanted overflow depending on the location of the positioning reference.
Frameworks
Rust Floating UI is available for the following frameworks:
The following frameworks are under consideration:
See Frameworks for documentation for each framework.
Examples
See Examples.
License
This project is available under the MIT license.
Rust For Web
The Rust Floating UI project is part of the Rust For Web.
Rust For Web creates and ports web UI libraries for Rust. All projects are free and open source.
Examples
Smart Anchor Positioning
Anchor a floating element next to another element while making sure it stays in view by avoiding collisions. This lets you position tooltips, popovers, or dropdowns optimally.
Compute Position
Computes coordinates to position a floating element next to another element.
use floating_ui_core::compute_position;
Usage
At its most basic, the function accepts two elements:
- Reference element - also known as the anchor element, this is the element that is being referred to for positioning. Often this is a button that triggers a floating popover like a tooltip or menu.
- Floating element - this is the element that floats next to the reference element, remaining anchored to it. This is the popover or tooltip itself.
use floating_ui_utils::Rect;
let reference_el = Rect {width: 100, height: 100, x 50, y: 50};
let floating_el = Rect {width: 200, height: 200, x 0, y: 0};
Then, call compute_position()
with them as arguments, ensuring you pass the required platform methods.
The first argument is the reference element to anchor to, and the second argument is the floating element to be positioned.
use floating_ui_core::{compute_position, ComputePositionConfig, ComputePositionReturn};
use floating_ui_utils::Rect;
let reference_el = Rect {width: 100, height: 100, x 50, y: 50};
let floating_el = Rect {width: 200, height: 200, x 0, y: 0};
let ComputePositionReturn {x, y} = compute_position(reference_el, floating_el, ComputePositionConfig::new(
// See https://floating-ui.rustforweb.org/platform.html
platform
));
// Paint the screen.
compute_position()
returns the coordinates that can be used to apply styles to the floating element.
By default, the floating element will be placed at the bottom center of the reference element
Options
Passed as a third argument, this is the struct instance to configure the behavior.
compute_position(
reference_el,
floating_el,
ComputePositionConfig::default(),
);
pub struct ComputePositionConfig<Element, Window> {
pub placement: Option<Placement>,
pub strategy: Option<Strategy>,
pub middleware: Option<Vec<Box<dyn Middleware<Element, Window>>>>,
}
placement
Where to place the floating element relative to its reference element. By default, this is Placement::Bottom
.
12 placements are available:
pub enum Placement {
Top,
TopStart,
TopEnd,
Right,
RightStart,
RightEnd,
Bottom,
BottomStart,
BottomEnd,
Left,
LeftStart,
LeftEnd,
}
compute_position(
reference_el,
floating_el,
ComputePositionConfig::default().placement(Placement::BottomStart),
);
The Start
and End
alignments are logical and will adapt to the writing direction (e.g. RTL) as expected.
Note
You aren't limited to just these 12 placements though. Offset
allows you to create any placement.
strategy
This is the type of CSS position property to use. By default, this is Strategy::Absolute
.
Two strategies are available:
pub enum Strategy {
Absolute,
Fixed,
}
compute_position(
reference_el,
floating_el,
ComputePositionConfig::default().strategy(Strategy::Fixed),
);
Ensure your initial layout matches the strategy:
#tooltip {
position: fixed;
}
These strategies are differentiated as follows:
Strategy::Absolute
- the floating element is positioned relative to its nearest positioned ancestor. With most layouts, this usually requires the browser to do the least work when updating the position.Strategy::Fixed
- the floating element is positioned relative to its nearest containing block (usually the viewport). This is useful when the reference element is also fixed to reduce jumpiness with positioning while scrolling. It will in many cases also “break” the floating element out of a clipping ancestor.
middleware
When you want granular control over how the floating element is positioned, middleware are used. They read the current coordinates, optionally alter them, and/or provide data for rendering. They compose and work together to produce the final coordinates which you receive as x
and y
parameters.
The following are included in the package:
Placement Modifiers
These middleware alter the base placement coordinates.
-
Offset
modifies the placement to add distance or margin between the reference and floating elements. -
Inline
positions the floating element relative to individual client rects rather than the bounding box for better precision.
Visibility Optimizers
These middleware alter the coordinates to ensure the floating element stays on screen optimally.
-
Shift
prevents the floating element from overflowing a clipping container by shifting it to stay in view. -
Flip
prevents the floating element from overflowing a clipping container by flipping it to the opposite placement to stay in view. -
AutoPlacement
automatically chooses a placement for you using a “most space” strategy. -
Size
resizes the floating element, for example so it will not overflow a clipping container, or to match the width of the reference element.
Data Providers
These middleware only provide data and do not alter the coordinates.
-
Arrow
provides data to position an inner element of the floating element such that it is centered to its reference element. -
Hide
provides data to hide the floating element in applicable situations when it no longer appears attached to its reference element due to different clipping contexts.
Custom
You can also craft your own custom middleware to extend the behavior of the library. Read middleware to learn how to create your own.
Conditional
Middleware can be called conditionally. Often this is useful for higher-level wrapper functions to avoid needing the consumer to import middleware themselves:
struct Options {
enable_flip: bool,
arrow_el: Option<Element>,
}
fn wrapper(reference_el: Element, floating_el: Element, options: Options) -> ComputePositionReturn {
let mut middleware = vec![];
if options.enable_flip {
middleware.push(Box::new(Flip::new(FlipOptions::default())));
}
if let Some(arrow_el) = options.arrow_el {
middleware.push(Box::new(Arrow::new(ArrowOptions::new(options.arrow_el))));
}
compute_position(reference_el, floating_el, ComputePositionConfig::default().middleware(middleware))
}
Return Value
compute_position()
returns the following struct:
pub struct ComputePositionReturn {
pub x: f64,
pub y: f64,
pub placement: Placement,
pub strategy: Strategy,
pub middleware_data: MiddlewareData,
}
x
The x-coordinate of the floating element.
y
The y-coordinate of the floating element.
placement
The final placement of the floating element, which may be different from the initial or preferred one passed in due to middleware modifications. This allows you to know which side the floating element is placed at.
strategy
The CSS position property to use.
middleware_data
The data returned by any middleware used.
See Also
Auto Update
Automatically updates the position of the floating element when necessary to ensure it stays anchored.
To ensure the floating element remains anchored to its reference element, such as when scrolling and resizing the screen, its position needs to be continually updated when necessary.
To solve this, auto_update()
adds listeners that will automatically call an update function which invokes compute_position()
when necessary.
Usage
It's important that this function is only called/set-up when the floating element is open on the screen, and cleaned up when it's removed. Otherwise, it can cause severe performance degradation, especially with many floating elements being created.
Unavailable API
This is a DOM API and is therefore not available for floating-ui-core
. If you are not using that package, change the package with the package switcher above.
Options
These are the options you can pass as a fourth argument to auto_update()
.
pub struct AutoUpdateOptions {
pub ancestor_scroll: Option<bool>,
pub ancestor_resize: Option<bool>,
pub element_resize: Option<bool>,
pub layout_shift: Option<bool>,
pub animation_frame: Option<bool>,
}
ancestor_scroll
Default: true
Whether to update the position when an overflow ancestor is scrolled.
auto_update(
reference_el,
floating_el,
update,
AutoUpdateOptions::default().ancestor_scroll(false),
);
ancestor_resize
Default: true
Whether to update the position when an overflow ancestor is resized. This uses the resize
event.
auto_update(
reference_el,
floating_el,
update,
AutoUpdateOptions::default().ancestor_resize(false),
);
element_resize
Default: true
Whether to update the position when either the reference or floating elements resized. This uses a ResizeObserver
.
auto_update(
reference_el,
floating_el,
update,
AutoUpdateOptions::default().element_resize(false),
);
layout_shift
Default: true
Whether to update the position of the floating element if the reference element moved on the screen as the result of layout shift. This uses a IntersectionObserver
.
auto_update(
reference_el,
floating_el,
update,
AutoUpdateOptions::default().layout_shift(false),
);
animation_frame
Default: false
Whether to update the position of the floating element on every animation frame if required. While optimized for performance, it should be used sparingly in the following cases:
- The reference element is animating on the screen with
transform
s. - Ensure a nested floating element is anchored when it's outside of ancestor floating elements' scrolling contexts.
auto_update(
reference_el,
floating_el,
update,
AutoUpdateOptions::default().animation_frame(true),
);
See Also
Middleware
Objects that change the positioning of the floating element, executed in order as a queue.
Middleware allow you to customize the behavior of the positioning and be as granular as you want, adding your own custom logic.
compute_position()
starts with initial positioning via 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:
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.position(Placement::Right)
.middleware(vec![]),
);
Example
use floating_ui_core::{Middleware, MiddlewareReturn, MiddlewareState};
const SHIFT_BY_ONE_PIXEL_NAME: &str = "shiftByOnePixel";
#[derive(Clone, PartialEq)]
struct ShiftByOnePixel {}
impl ShiftByOnePixel {
pub fn new() -> Self {
ShiftByOnePixel {}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for ShiftByOnePixel
{
fn name(&self) -> &'static str {
SHIFT_BY_ONE_PIXEL_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
MiddlewareReturn {
x: Some(state.x + 1.0),
y: Some(state.y + 1.0),
data: None,
reset: None,
}
}
}
This (not particularly useful) middleware adds 1
pixel to the coordinates. To use this middleware, add it to your middleware
vector:
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.position(Placement::Right)
.middleware(vec![
Box::new(ShiftByOnePixel::new())
]),
);
Here, compute_position()
will compute coordinates that will place the floating element to the 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 a struct that implements the Middleware
trait. It has a name
and a compute
method The compute
method provides the logic of the middleware, which returns new positioning coordinates or useful data.
Data
Any data can be passed via an optional data
field of the struct instance that is returned from compute
. This will be accessible to the consumer via the middleware_data
field:
use floating_ui_core::{Middleware, MiddlewareReturn, MiddlewareState};
use serde::{Deserialize, Serialize};
const SHIFT_BY_ONE_PIXEL_NAME: &str = "shiftByOnePixel";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct ShiftByOnePixelData {
pub amount: f64,
}
#[derive(Clone, PartialEq)]
struct ShiftByOnePixel {}
impl ShiftByOnePixel {
pub fn new() -> Self {
ShiftByOnePixel {}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for ShiftByOnePixel
{
fn name(&self) -> &'static str {
SHIFT_BY_ONE_PIXEL_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
MiddlewareReturn {
x: Some(state.x + 1.0),
y: Some(state.y + 1.0),
data: Some(
serde_json::to_value(ShiftByOnePixelData {
amount: 1.0,
})
.expect("Data should be valid JSON."),
),
reset: None,
}
}
}
let ComputePositionReturn {
middleware_data,
..
} = compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.position(Placement::Right)
.middleware(vec![
Box::new(ShiftByOnePixel::new())
]),
);
if let Some(data) = middleware_data.get_as::<ShiftByOnePixelData>(SHIFT_BY_ONE_PIXEL_NAME) {
log::info!("{:#?}", data);
}
Options
Options can be passed to the middleware and stored in the struct:
use floating_ui_core::{Middleware, MiddlewareReturn, MiddlewareState};
const SHIFT_BY_ONE_PIXEL_NAME: &str = "shiftByOnePixel";
#[derive(Clone, Debug, Default, PartialEq)]
struct ShiftByOnePixelOptions {
amount: f64,
}
impl ShiftByOnePixelOptions {
pub fn amount(mut self, value: f64) -> Self {
self.amount = value;
self
}
}
#[derive(Clone, PartialEq)]
struct ShiftByOnePixel {
options: ShiftByOnePixelOptions,
}
impl ShiftByOnePixel {
pub fn new(options: ShiftByOnePixelOptions) -> Self {
ShiftByOnePixel {
options,
}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for ShiftByOnePixel
{
fn name(&self) -> &'static str {
SHIFT_BY_ONE_PIXEL_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
MiddlewareReturn {
x: Some(state.x + self.options.amount),
y: Some(state.y + self.options.amount),
data: None,
reset: None,
}
}
}
The options can be passed to the middleware to configure the behavior:
let middleware = vec![Box::new(ShiftByOnePixel::new(ShiftByOnePixelOptions::default().amount(10)))];
Middleware State
A struct instance is passed to compute
containing useful data about the middleware lifecycle being executed.
In the previous examples, we used x
and y
out of the compute
parameter struct. These are only two fields that get passed into middleware, but there are many more.
The fields passed are below:
pub struct MiddlewareState<'a, Element: Clone + 'static, Window: Clone> {
pub x: f64,
pub y: f64,
pub initial_placement: Placement,
pub placement: Placement,
pub strategy: Strategy,
pub middleware_data: &'a MiddlewareData,
pub elements: Elements<'a, Element>,
pub rects: &'a ElementRects,
pub platform: &'a dyn Platform<Element, Window>,
}
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 a struct instance containing the reference and floating elements.
rects
This is a struct instance containing the Rect
s of the reference and floating elements, a struct of shape {x, y, width, height}
.
middleware_data
This is a struct instance containing all the data of any middleware at the current step in the lifecycle. The lifecycle loops over the middleware
vector, so later middleware have access to data from any middleware run prior.
strategy
The positioning strategy.
initial_placement
The initial (or preferred) placement passed in to compute_position()
.
placement
The stateful resultant placement. Middleware like Flip
change initial_placement
to a new one.
platform
A struct instance containing methods to make Floating UI work on the current platform, e.g. DOM.
Ordering
The order in which middleware are placed in the vector 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
in the middleware vector means the coordinates get shifted by 3 pixels in total:
let middleware = vec![
Box::new(ShiftByOnePixel::new()),
Box::new(ShiftByOnePixel::new()),
Box::new(ShiftByOnePixel::new()),
];
If the later ShiftByOnePixel
implementations had a condition based on the current value of x
and y
, the condition can change based on their placement in the vector.
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
should always go at the beginning of the middleware vector, while Arrow
and Hide
at the end. The other core middleware can be shifted around depending on the desired behavior.
let middleware = vec![
Box::new(Offset::new(OffsetOptions::default())),
// ...
Box::new(Arrow::new(ArrowOptions::new(arrow_element))),
Box::new(Hide::new(HideOptions::default())),
];
Resetting the Lifecycle
There are use cases for needing to reset the middleware lifecycle so that other middleware perform fresh logic.
- When
Flip
andAutoPlacement
change the placement, they reset the lifecycle so that other middleware that modify the coordinates based on the currentplacement
do not perform stale logic. Size
resets the lifecycle with the newly applied dimensions, as many middleware read the dimensions to perform their logic.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
field to the struct instance returned from compute
.
pub enum Reset {
True,
Value(ResetValue),
}
pub struct ResetValue {
pub placement: Option<Placement>,
pub rects: Option<ResetRects>,
}
// `True` will compute the new `rects` if the dimensions were mutated.
// Otherwise, you can return your own new rects.
pub enum ResetRects {
True,
Value(ElementRects),
}
const SOME_NAME: &str = "some";
#[derive(Clone, PartialEq)]
struct SomeMiddleware {}
impl SomeMiddleware {
pub fn new() -> Self {
SomeMiddleware {}
}
}
impl<Element: Clone + PartialEq, Window: Clone + PartialEq> Middleware<Element, Window>
for SomeMiddleware
{
fn name(&self) -> &'static str {
SOME_NAME
}
fn compute(&self, state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
if some_condition {
MiddlewareReturn {
x: None,
y: None,
data: None,
reset: Some(Reset::Value(ResetValue {
placements: Some(next_placement),
reset: None
})),
}
} else {
MiddlewareReturn {
x: None,
y: None,
data: None,
reset: None,
}
}
}
}
Data supplied to middleware_data
is preserved by doing this, so you can read it at any point after you've reset the lifecycle.
See Also
Arrow
Provides positioning data for an arrow element (triangle or caret) inside the floating element, such that it appears to be pointing toward the center of the reference element.
This is useful to add an additional visual cue to the floating element about which element it is referring to.
TODO
See Also
Auto Placement
Chooses the placement that has the most space available automatically.
This is useful when you don't know which placement will be best for the floating element, or don't want to have to explicitly specify it.
TODO
See Also
Flip
Changes the placement of the floating element to keep it in view.
This prevents the floating element from overflowing along its side axis by flipping it to the opposite side by default.
TODO
See Also
Hide
A data provider that allows you to hide the floating element in applicable situations.
This is useful for situations where you want to hide the floating element because it appears detached from the reference element (or attached to nothing).
TODO
See Also
Inline
Improves positioning for inline reference elements that span over multiple lines.
This is useful for reference elements such as hyperlinks or range selections, as the default positioning using getBoundingClientRect()
may appear “detached” when measuring over the bounding box.
TODO
See Also
Offset
Translates the floating element along the specified axes.
This lets you add distance (margin or spacing) between the reference and floating element, slightly alter the placement, or even create custom placements.
Type: Placement Modifier
Usage
use floating_ui_core::{compute_position, ComputePositionConfig, Offset, OffsetOptions};
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.middleware(vec![
Box::new(Offset::new(OffsetOptions::default())),
]),
);
The value(s) passed are logical, meaning their effect on the physical result is dependent on the placement, writing direction (e.g. RTL), or alignment.
Order
Offset
should generally be placed at the beginning of your middleware vector.
Options
These are the options you can pass to Offset
.
pub enum OffsetOptions {
Value(f64),
Values(OffsetOptionsValues),
}
pub struct OffsetOptionsValues {
pub main_axis: Option<f64>,
pub cross_axis: Option<f64>,
pub alignment_axis: Option<f64>,
}
A single number represents the distance (gutter or margin) between the floating element and the reference element. This is shorthand for main_axis
.
Offset::new(OffsetOptions::Value(10.0))
A struct instance can also be passed, which enables you to individually configure each axis.
main_axis
Default: 0.0
The axis that runs along the side of the floating element. Represents the distance (gutter or margin) between the floating element and the reference element.
Offset::new(OffsetOptions::Values(
OffsetOptionsValues::default().main_axis(10.0)
))
cross_axis
Default: 0.0
The axis that runs along the alignment of the floating element. Represents the skidding between the floating element and the reference element.
Offset::new(OffsetOptions::Values(
OffsetOptionsValues::default().cross_axis(20.0)
))
alignment_axis
Default: None
The same axis as cross_axis
but applies only to aligned placements and inverts the End
alignment. When set to a number, it overrides the cross_axis
value.
A positive number will move the floating element in the direction of the opposite edge to the one that is aligned, while a negative number the reverse.
Offset::new(OffsetOptions::Values(
OffsetOptionsValues::default().alignment_axis(20.0)
))
See Also
Shift
Shifts the floating element to keep it in view.
This prevents the floating element from overflowing along its axis of alignment, thereby preserving the side it's placed on.
Type: Visibility Optimizer
Usage
use floating_ui_core::{compute_position, ComputePositionConfig, Shift, ShiftOptions};
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(platform)
.middleware(vec![
Box::new(Shift::new(ShiftOptions::default())),
]),
);
Options
These are the options you can pass to Shift
.
pub struct ShiftOptions<Element, Window> {
pub detect_overflow: Option<DetectOverflowOptions<Element>>,
pub main_axis: Option<bool>,
pub cross_axis: Option<bool>,
pub limiter: Option<Box<dyn Limiter<Element, Window>>>,
}
pub trait Limiter<Element, Window>: Clone + PartialEq {
fn compute(&self, state: MiddlewareState<Element, Window>) -> Coords;
}
main_axis
Default: true
This is the main axis in which shifting is applied.
x
-axis forTop
andBottom
placementsy
-axis forLeft
andRight
placements
Shift::new(ShiftOptions::default().main_axis(false))
cross_axis
Default: false
This is the cross axis in which shifting is applied, the opposite axis of main_axis
.
Enabling this can lead to the floating element overlapping the reference element, which may not be desired and is often replaced by the Flip
middleware.
Shift::new(ShiftOptions::default().cross_axis(true))
limiter
Default: no-op
This accepts a struct instance that limits the shifting done, in order to prevent detachment or “overly-eager” behavior. The behavior is to stop shifting once the opposite edges of the elements are aligned.
Shift::new(ShiftOptions::default().limiter(
Box::new(LimitShift::new(
LimitShiftOptions::default()
)),
))
This struct itself takes options.
pub struct LimitShiftOptions<'a, Element, Window> {
pub offset: Option<Derivable<'a, Element, Window, LimitShiftOffset>>,
pub main_axis: Option<bool>,
pub cross_axis: Option<bool>,
}
pub enum LimitShiftOffset {
Value(f64),
Values(LimitShiftOffsetValues),
}
pub struct LimitShiftOffsetValues {
pub main_axis: Option<f64>,
pub cross_axis: Option<f64>,
}
main_axis
Default: true
Whether to apply limiting on the main axis.
Shift::new(ShiftOptions::default().limiter(
Box::new(LimitShift::new(
LimitShiftOptions::default().main_axis(false)
)),
))
cross_axis
Default: true
Whether to apply limiting on the cross axis.
Shift::new(ShiftOptions::default().limiter(
Box::new(LimitShift::new(
LimitShiftOptions::default().cross_axis(false)
)),
))
offset
Default: 0.0
This will offset when the limiting starts. A positive number will start limiting earlier, while negative later.
Shift::new(ShiftOptions::default().limiter(
Box::new(LimitShift::new(
// Start limiting 5px earlier
LimitShiftOptions::default().offset(LimitShiftOffset::Value(5.0)),
)),
))
You may also pass a struct instance to configure both axes:
Shift::new(ShiftOptions::default().limiter(
Box::new(LimitShift::new(
LimitShiftOptions::default().offset(LimitShiftOffset::Values(
LimitShitOffsetValues::default()
.main_axis(10.0)
.cross_axis(5.0),
)),
)),
))
detect_overflow
All of detect_overflow
's options can be passed. For instance:
Shift::new(ShiftOptions::default().detect_overflow(
DetectOvverflowOptions::default().padding(Padding::All(5.0)),
))
Data
The following data is available in middleware_data
under the SHIFT_NAME
key:
middleware_data.get_as::<ShiftData>(SHIFT_NAME)
pub struct ShiftData {
pub x: f64,
pub y: f64,
}
x
and y
represent how much the floating element has been shifted along that axis. The values are offsets, and therefore can be negative.
See Also
Size
Provides data to change the size of a floating element.
This is useful to ensure the floating element isn't too big to fit in the viewport (or more specifically, its clipping context), especially when a maximum size isn't specified. It also allows matching the width/height of the reference element.
TODO
See Also
Detect Overflow
Detects when the floating or reference element is overflowing a clipping container or custom boundary.
TODO
See Also
Virtual Elements
Position a floating element relative to a custom reference area, useful for context menus, range selections, following the cursor, and more.
Usage
A virtual element must implement the VirtualElement
trait.
pub trait VirtualElement<Element>: Clone + PartialEq {
fn get_bounding_client_rect(&self) -> ClientRectObject;
fn get_client_rects(&self) -> Option<Vec<ClientRectObject>>;
fn context_element(&self) -> Option<Element>;
}
A default implementation called DefaultVirtualElement
is provided for convience.
let virtual_el: Box<dyn VirtualElement<Element>> = Box::new(
DefaultVirtualElement::new(get_bounding_client_rect)
.get_client_rects(get_client_rects)
.context_element(context_element),
);
compute_position(virtual_el.into(), floating_el, ComputePositionConfig::new(platform))
get_bounding_client_rect
The most basic virtual element is a plain object that has a get_bounding_client_rect
method, which mimics a real element's one:
// A virtual element that is 20 x 20 px starting from (0, 0)
let virtual_el: Box<dyn VirtualElement<Element>> = Box::new(
DefaultVirtualElement::new(Rc::new(|| {
ClientRectObject {
x: 0.0,
y: 0.0,
top: 0.0,
left: 0.0,
bottom: 20.0,
right: 20.0,
width: 20.0,
height: 20.0,
}
}))
);
context_element
This option is useful if your get_bounding_client_rect
method is derived from a real element, to ensure clipping and position update detection works as expected.
let virtual_el: Box<dyn VirtualElement<Element>> = Box::new(
DefaultVirtualElement::new(get_bounding_client_rect)
.context_element(
web_sys::window()
.expext("Window should exist.")
.document()
.expect("Document should exist.")
.query_selector("#context")
.expect("Document should be queried.")
.expect("Element should exist."),
),
);
get_client_rects
This option is useful when using range selections and the Inline
middleware.
let virtual_el: Box<dyn VirtualElement<Element>> = Box::new(
DefaultVirtualElement::new(|| range.get_bounding_client_rect().into())
.get_client_rects(|| ClientRectObject::from_dom_rect_list(
range.get_client_rects().expect("Range should have client rects."),
)),
);
See Also
Platform
Use Floating UI's positioning logic on any platform that can execute Rust.
Floating UI's core is essentially a bunch of mathematical calculations performed on rectangles. These calculations are pure and agnostic, allowing Floating UI to work on any platform that can execute Rust.
To make it work with a given platform, methods are used to allow it to hook into measurement APIs, for instance, to measure the bounding box of a given element.
Possible platforms other than the DOM include Native, Canvas/WebGL, etc.
Custom Platform Struct
If you're building a platform from scratch, e.g. your own tiny custom DOM platform, you'll be using the floating-ui-core
package - see Methods.
If you're extending or customizing the existing DOM methods, and are using floating-ui-dom
, this is accessible via the Platform
import - see Extending the DOM Platform.
Shadow DOM Fix
TODO
Concepts
The library works largely with a Rect
:
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
This data can come from anywhere, and the library will perform the right computations. x
and y
represent the coordinates of the element relative to another one.
Methods
A platform
is a struct implementing the Platform
trait, which consists of 3 required and 7 optional methods. These methods allow the platform to interface with Floating UI's logic.
The Platform<Element, Window>
trait has two generic types that reflect the element and window of the platform. The DOM platform uses web_sys::Element
and web_sys::Window
.
Required Methods
get_element_rects
Takes in the elements and the positioning strategy
and returns the element Rect
struct instances.
pub fn get_element_rects(
GetElementRectsArgs {
reference,
floating,
strategy,
}: GetElementRectsArgs<Element>
) -> ElementRects {
ElementRects {
reference: Rect {x: 0.0, y: 0.0, width: 0.0, height: 0.0},
floating: Rect {x: 0.0, y: 0.0, width: 0.0, height: 0.0},
}
}
reference
The x
and y
values of a reference Rect
should be its coordinates relative to the floating element's offset_parent
element if required rather than the viewport.
floating
Both x
and y
are not relevant initially, so you can set these both of these to 0.0
.
get_dimensions
pub fn get_dimensions(element: &Element) -> Dimensions {
Dimensions {
width: 0.0,
height: 0.0,
}
}
get_clipping_rect
Returns the Rect
(relative to the viewport) whose outside bounds will clip the given element. For instance, the viewport itself.
pub fn get_clipping_rect(
GetClippingRectArgs {
element,
boundary,
root_boundary,
strategy,
}: GetClippingRectArgs<Element>,
) -> Rect {
Rect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
}
}
Optional Methods
Depending on the platform you're working with, these may or may not be necessary.
convert_offset_parent_relative_rect_to_viewport_relative_rect
This function will take a Rect
that is relative to a given offset_parent
element and convert its x
and y
values such that it is instead relative to the viewport.
pub fn convert_offset_parent_relative_rect_to_viewport_relative_rect(
ConvertOffsetParentRelativeRectToViewportRelativeRectArgs {
elements,
rect,
offset_parent,
strategy,
}: ConvertOffsetParentRelativeRectToViewportRelativeRectArgs<Element, Window>,
) -> Rect {
rect
}
get_offset_parent
Returns the offset_parent
of a given element.
pub fn get_offset_parent(
element: &Element,
polyfill: Option<Polyfill>,
) -> OwnedElementOrWindow<Element, Window> {
OwnedElementOrWindow::Window(window)
}
The polyfill parameter exists only for floating-ui-dom
and is optional to fix the
Shadow DOM Bug.
get_document_element
Returns the document element.
fn get_document_element(element: &Element) -> Element {
document_element
}
get_client_rects
Returns a vector of ClientRect
s.
fn get_client_rects(element: ElementOrVirtual) -> Vec<ClientRectObject> {
vec![]
}
is_rtl
Determines if an element is in RTL layout.
fn is_rtl(element: &Element) -> bool {
false
}
get_scale
Determines the scale of an element.
fn get_scale(element: &Element) -> Coords {
Coords {
x: 1.0,
y: 1.0
}
}
get_client_length
Returns the client width or height of an element.
fn get_client_length(element: &Element, length: Length) -> f64 {
match length {
Length::Width => 0.0,
Length::Height => 0.0,
}
}
Usage
All these methods are passed in the implementation of the Platform
trait.
use floating_ui_core::{compute_position, ComputePositionConfig, Platform};
#[derive(Debug)]
pub struct CustomPlatform {}
impl Platform<Element, Window> for CustomPlatform {
// Required
fn get_element_rects(&self, args: GetElementRectsArgs<Element>) -> ElementRects {
get_element_rects(args)
}
fn get_dimensions(&self, element: &Element) -> Dimensions {
get_dimensions(element)
}
fn get_clipping_rect(&self, args: GetClippingRectArgs<Element>) -> Rect {
get_clipping_rect(args)
}
// Optional (pass `None` if not implemented)
fn convert_offset_parent_relative_rect_to_viewport_relative_rect(
&self,
args: ConvertOffsetParentRelativeRectToViewportRelativeRectArgs<Element, Window>,
) -> Option<Rect> {
Some(convert_offset_parent_relative_rect_to_viewport_relative_rect(args))
}
fn get_offset_parent(
&self,
element: &Element,
) -> Option<OwnedElementOrWindow<Element, Window>> {
Some(get_offset_parent(element, None))
}
fn get_document_element(&self, element: &Element) -> Option<Element> {
Some(get_document_element(element))
}
fn get_client_rects(&self, element: ElementOrVirtual) -> Option<Vec<ClientRectObject>> {
Some(get_client_rects(element))
}
fn is_rtl(&self, element: &Element) -> Option<bool> {
Some(is_rtl(element))
}
fn get_scale(&self, element: &Element) -> Option<Coords> {
Some(get_scale(element))
}
fn get_client_length(
&self,
element: &Element,
length: floating_ui_utils::Length,
) -> Option<f64> {
Some(get_client_length(element, length))
}
}
const PLATFORM: CustomPlatform = CustomPlatform {};
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(&PLATFORM),
);
Extending the DOM Platform
use floating_ui_core::{compute_position, ComputePositionConfig, Platform};
use floating_ui_dom::Platform as DomPlatform;
use web_sys::{Element, Window};
#[derive(Debug)]
struct CustomPlatform {
dom_platform: DomPlatform,
}
impl Platform<Element, Window> for CustomPlatform {
// Use existing DOM methods.
fn get_element_rects(&self, args: GetElementRectsArgs<Element>) -> ElementRects {
self.dom_platform.get_element_rects(self, args)
}
// Overwrite methods with your own.
fn get_dimensions(&self, element: &Element) -> Dimensions {
Dimensions {
width: 0.0,
height: 0.0,
}
}
// Etc.
}
const PLATFORM: CustomPlatform = CustomPlatform {
dom_platform: DomPlatform {}
};
compute_position(
reference_el,
floating_el,
ComputePositionConfig::new(&PLATFORM),
);
See Also
Frameworks
Rust Floating UI is available for the following frameworks:
DOM
This package provides DOM (web-sys
) bindings for floating-ui-core
- a library that provides anchor positioning for a floating element to position it next to a given reference element.
Installation
cargo add floating-ui-dom
Usage
TODO
Leptos
This package provides Leptos bindings for floating-ui-dom
- a library that provides anchor positioning for a floating element to position it next to a given reference element.
Installation
cargo add floating-ui-leptos
Usage
use_floating
is the main composable:
use floating_ui_leptos::{use_floating, UseFloatingOptions, UseFloatingReturn};
use leptos::prelude::*;
#[component]
pub fn Example() -> impl IntoView {
let reference_ref = NodeRef::new();
let floating_ref = NodeRef::new();
let UseFloatingReturn {
floating_styles,
} = use_floating(reference_ref, floating_ref, UseFloatingOptions::default());
view! {
<button node_ref=reference_ref>Button</button>
<div
node_ref=floating_ref
style:position=move || floating_styles.get().style_position()
style:top=move || floating_styles.get().style_top()
style:left=move || floating_styles.get().style_left()
style:transform=move || floating_styles.get().style_transform()
style:will-change=move || floating_styles.get().style_will_change()
>
Tooltip
</div>
}
}
This will position the floating Tooltip
element at the bottom center of the Button
element by default.
reference
is the reference (or anchor) element that is being referred to for positioning.floating
is the floating element that is being positioned relative to the reference element.floating_styles
is a signal of positioning styles to apply to the floating element'sstyle
attribute.
Disabling Transform
By default, the floating element is positioned using transform
in the floating_styles
struct instance. This is the most performant way to position elements, but can be disabled:
use_floating(reference_ref, floating_ref, UseFloatingOptions::default().transform(false.into()));
If you'd like to retain transform styles while allowing transform animations, create a wrapper, where the outermost node is the positioned one, and the inner is the actual styled element.
Custom Position Styles
The composable returns the coordinates and positioning strategy directly if floating_styles
is not suitable for full customization.
let UseFloatingReturn {
x,
y,
strategy,
} = use_floating(reference_ref, floating_ref, UseFloatingOptions::default());
Return Value
The composable returns all the values from compute_position
, plus some extras to work with Leptos. This includes data about the final placement and middleware data which are useful when rendering.
Options
The composable accepts all the options from compute_position
, which allows you to customize the position. Here's an example:
use floating_ui_leptos::{
use_floating, Flip, FlipOptions, MiddlewareVec, Offset, OffsetOptions, Placement, Shift, ShiftOptions, UseFloatingOptions
};
let middleware: MiddlewareVec = vec![
Box::new(Offset::new(OffsetOptions::Value(10.0))),
Box::new(Flip::new(FlipOptions::default())),
Box::new(Shift::new(ShiftOptions::default())),
];
use_floating(
reference_ref,
floating_ref,
UseFloatingOptions::default()
.placement(Placement::Right.into())
.middleware(middleware.into())
);
The composable also accepts Signal
options:
use floating_ui_leptos::{
use_floating, Flip, FlipOptions, MiddlewareVec, Offset, OffsetOptions, Placement, Shift, ShiftOptions, UseFloatingOptions
};
use leptos::prelude::*;
let placement = Signal::derive(move || Placement::Right);
let middleware: Signal<MiddlewareVec> = Signal::derive(move || vec![
Box::new(Offset::new(OffsetOptions::Value(10.0))),
Box::new(Flip::new(FlipOptions::default())),
Box::new(Shift::new(ShiftOptions::default())),
]);
use_floating(
reference_ref,
floating_ref,
UseFloatingOptions::default()
.placement(placement.into())
.middleware(middleware.into())
);
Middleware can alter the positioning from the basic placement
, act as visibility optimizers, or provide data to use.
The docs for the middleware that were passed are available here:
Anchoring
The position is only calculated once on render, or when the reference
or floating
elements changed - for example, the floating element get mounted via conditional rendering.
To ensure the floating element remains anchored to its reference element in a variety of scenarios without detaching - such as when scrolling or resizing the page - you can pass the auto_update
utility to the while_elements_mounted
option:
use_floating(
reference_ref,
floating_ref,
UseFloatingOptions::default().while_elements_mounted_auto_update(),
);
To pass options to auto_update
:
use_floating(
reference_ref,
floating_ref,
UseFloatingOptions::default().while_elements_mounted_auto_update_with_options(
AutoUpdateOptions::default().animation_frame(true).into()
),
);
Ensure you are using conditional rendering (Show
) for the floating element, not an opacity/visibility/display style. If you are using the latter, avoid the while_elements_mounted
option.
Manual Updating
While auto_update
covers most cases where the position of the floating element must be updated, it does not cover every single one possible due to performance/platform limitations.
The composable returns an update()
function to update the position at will:
let UseFloatingReturn {
update,
} = use_floating(reference_ref, floating_ref, UseFloatingOptions::default());
view! {
<button on:click=move || update()>Update</button>
}
Effects
TODO
Arrow
TODO
Virtual Element
TODO
Yew
This package provides Yew bindings for floating-ui-dom
- a library that provides anchor positioning for a floating element to position it next to a given reference element.
Installation
cargo add floating-ui-yew
Usage
TODO