Rust Floating UI Logo

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 transforms.
  • 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 Rects 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 and AutoPlacement change the placement, they reset the lifecycle so that other middleware that modify the coordinates based on the current placement 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 for Top and Bottom placements
  • y-axis for Left and Right 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 ClientRects.

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's style 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