# float Places 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. ```ts Signatures function 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>` | `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"`, ``) 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 ```ehtml app/shared/templates/hint/index.ehtml import { float, type FloatStop } from "@elements/ui/behaviors"; interface HintState { anchor?: HTMLElement; stop?: FloatStop; } ui.anchor = e.target as HTMLElement} onpointerenter={() => open = true} onpointerleave={() => open = false}> ``` ```css app/shared/templates/hint/style.css .hint { transform-origin: var(--transform-origin); max-width: var(--available-width); } ``` ## Used by `float`: [DropdownMenu](../templates/dropdown-menu), [ContextMenu](../templates/context-menu), [Menubar](../templates/menubar), [Message](../templates/message), [NavigationMenu](../templates/navigation-menu), [Popover](../templates/popover), [RichText](../templates/rich-text), [Select](../templates/select), [TagsInput](../templates/tags-input), [Toast](../templates/toast). `computeFloat`: [Select](../templates/select), to line the selected option up over the trigger.