float
elements man ui/behaviors/floatingPlaces a floating element (a popover, menu, tooltip or listbox) against an
anchor, keeps it there while the page scrolls or resizes, and flips or
shifts it to stay on screen. float positions and follows until stopped;
computeFloat finds the position once without moving anything.
Signaturesfunction float(anchor: Anchor, floating: HTMLElement, opts?: FloatOptions): FloatStop; function computeFloat(anchor: Anchor, floating: HTMLElement, opts?: FloatOptions, onSize?: (size: FloatSize) => void): FloatPlacement; type Anchor = Element | { getBoundingClientRect(): DOMRect; contextElement?: Element }; type FloatStop = (() => void) & { update(): void }; type Side = "top" | "right" | "bottom" | "left"; type LogicalSide = Side | "inline-start" | "inline-end"; type Align = "start" | "center" | "end";
The anchor may be any object with getBoundingClientRect(), such as a point
under the pointer, with an optional contextElement that scroll containers
and direction are read from. The returned stop also has update(), which
places the element again at once: a pointer anchor that moved, content the
observers cannot see.
Options
| Option | Type | Default | What it does |
|---|---|---|---|
side |
LogicalSide |
"bottom" |
Preferred side. inline-start and inline-end follow the anchor's direction. |
align |
Align |
"center" |
Alignment along that side. start and end follow the writing direction on the top and bottom sides. |
offset |
number | OffsetFn |
4 |
Gap between anchor and floating element, px, or a function of the placement being tried. |
alignOffset |
number | OffsetFn |
0 |
Shift along the side, px, or a function of the placement. Ignored when align is center. |
flip |
boolean |
true |
Move to the opposite side when there is no room. |
crossAxis |
boolean |
false |
Flip also when the box overflows along its side: try the other alignment first, so a context menu near the bottom edge ends at the press instead of sliding over it. |
crossAxisFallback |
boolean |
true |
When nothing fits on the preferred side or its opposite, try the two sides of the other axis too. false keeps the box on its axis, where the size step shrinks it, as a menu does. |
crossAxisFallbackSide |
"start" | "end" |
"end" |
Which side of the other axis that fallback tries first: "end" tries right before left (left first under rtl) and below before above; "start" the reverse. |
sideShift |
boolean |
false |
Keep the side and slide the box along the side axis back into the boundary, over the anchor if it must. Pair it with flip: false. |
shift |
boolean |
true |
Slide along the side to stay in view. |
padding |
number | Partial<Record<Side, number>> |
8 |
Distance kept from the boundary edges, px, or per side; a side left out keeps 8. |
matchWidth |
boolean |
false |
Set min-width to the anchor's width. |
arrow |
HTMLElement | null |
none | An arrow element inside the floating element to point at the anchor. |
arrowPadding |
number |
6 |
px the arrow keeps from the floating element's corners. |
sticky |
"partial" | "always" |
"partial" |
"partial" lets shift stop when the anchor leaves; "always" keeps the element in the boundary on both axes, so an anchor scrolled past an edge leaves its box at that edge. |
boundary |
Element[] |
the viewport | Elements whose edges count as collision edges, besides the viewport. |
updatePositionStrategy |
"optimized" | "always" |
"optimized" |
"optimized" updates on scroll, resize and layout changes; "always" every frame. |
hideWhenDetached |
boolean |
true |
Hide (visibility) when the anchor scrolls out of view. |
strategy |
"fixed" | "absolute" |
"fixed" |
The CSS position used. |
onPlaced |
(side: Side, align: Align) => void |
none | Called after each placement with the final side and align. |
autoPlacement |
boolean | { allowedSides?: Side[] } |
false |
Pick the side with the most room instead of a preferred one. allowedSides limits the choice; flip is skipped, shift still applies. |
inline |
boolean | { x?: number; y?: number } |
false |
The anchor is inline text over several lines: place against the line facing the side, not the whole block. Top uses the first line, bottom the last; a point picks the line under it. |
middleware |
FloatMiddleware[] |
none | Own steps run after shift on every pass, in order. Each gets a MiddlewareState (x, y, side, align, anchor and floating boxes in viewport px) and may return a new x or y. |
An OffsetFn gets OffsetData: the side and align being tried and the
anchor and floating sizes in CSS px, and returns px, so a gap can
differ per side.
How a position is found
Each step sees the previous one's result. Base: the box beside the anchor
on side, aligned by align, pushed offset away and alignOffset
along. Flip: the preferred placement, then the same side with the other
alignment, the opposite side, and the two sides of the other axis; the
first that fits wins, else the one that overflows least. Shift: slide along
the side into the boundary, keeping padding. Size: the room left on each
axis, written as CSS variables; when that changes the box's size the steps
run again. Arrow: centered on the anchor, kept arrowPadding from the
corners. Hidden: whether the anchor is clipped out of every scroll
container.
In the top layer (popover="manual", <dialog>) left and top are viewport
coordinates; under a transformed or contained ancestor they are converted
into that ancestor's box, and under a scaled ancestor the gap, room and
arrow are corrected for the scale.
Writes
| On the floating element | What it is |
|---|---|
position, left, top |
The placement, in the strategy given. |
data-side, data-align |
The side and alignment it ended up on, after flip. |
data-anchor-hidden |
Present while the anchor is scrolled out of view; visibility: hidden goes with it. |
--anchor-width, --anchor-height |
The anchor's size, for min-width: var(--anchor-width) on a listbox. |
--available-width, --available-height |
Room left before the boundary edge, for max-height: var(--available-height) on a menu. |
--transform-origin |
The anchor-facing edge, for a scale-in animation. |
On the arrow: left or top, data-side, and data-uncentered when the
arrow cannot point at the anchor's middle.
computeFloat returns a FloatPlacement instead of writing: x, y,
side, align, availableWidth, availableHeight, anchorWidth,
anchorHeight, arrow (its offset, or null), uncentered and
anchorHidden. The one thing it sets is position on the floating element
and on the arrow, so the measure is right.
In a template of your own
app/shared/templates/hint/index.ehtmlimport { float, type FloatStop } from "@elements/ui/behaviors"; interface HintState { anchor?: HTMLElement; stop?: FloatStop; } <Hint (text: string, private open: boolean = false, private ui: HintState = {})> <span class="hint-anchor" oninsert={(e) => ui.anchor = e.target as HTMLElement} onpointerenter={() => open = true} onpointerleave={() => open = false}><slot/></span> <div e:if={open} class="hint" role="tooltip" oninsert={(e) => ui.stop = float(ui.anchor!, e.target as HTMLElement, { side: "top", offset: 6 })} onremove={() => ui.stop?.()}>{text}</div> </Hint>
app/shared/templates/hint/style.css.hint { transform-origin: var(--transform-origin); max-width: var(--available-width); }
Used by
float: DropdownMenu (opens in a new tab), ContextMenu (opens in a new tab),
Menubar (opens in a new tab), Message (opens in a new tab),
NavigationMenu (opens in a new tab), Popover (opens in a new tab),
RichText (opens in a new tab), Select (opens in a new tab),
TagsInput (opens in a new tab), Toast (opens in a new tab).
computeFloat: Select (opens in a new tab), to line the selected option
up over the trigger.