Absolute positioning resolves against immediate parent, not the nearest positioned ancestor (CSS containing-block semantics)
Summary
Follow-up to #406 / #413. After #413, absolute elements accumulate their parent's screen offset, so they position relative to their immediate parent (matching Yoga's parent-relative layout). That's closer to browsers, but still not CSS semantics: in web CSS, left/top of an absolutely positioned element resolve against its containing block — the padding box of the nearest positioned ancestor (or the initial containing block / viewport when no ancestor is positioned).
The missing piece: the nearest positioned ancestor selection. Today the containing block is always the immediate parent, regardless of whether it is positioned.
Bonus bug: position: "static" is currently a no-op — explicitly setting it changes nothing (adding it to the repro below produces identical output). In CSS, static means the element is not a containing block; the flag existing with zero observable effect is arguably a bug on its own.
Repro
export const Application = () => {
return (
<box width="100%" height="100%">
<box style={{ flexGrow: 1 }} />
{/* static container, sits at screen x=10 */}
<box flexShrink={0} style={{ height: 4, marginLeft: 10 }}>
<box
style={{
position: "absolute",
left: 20,
top: 0,
width: 5,
height: 1,
}}
>
<text>lands at x=30; CSS would place it at viewport x=20</text>
</box>
</box>
</box>
);
};In a browser (static ancestors, no positioned ancestor) the absolute box's containing block is the viewport, so left: 20 renders at viewport x=20. In opentui it renders at parent.x + 20 = 30. Setting position: "relative" on the container makes both agree — so the divergence is specifically the static-ancestor case. Adding position: "static" to the container changes nothing (see bonus bug above).
Why it matters
floating-ui / zag-style placement (tooltips, popovers, dropdowns, command palettes) computes viewport coordinates (getBoundingClientRect is viewport-relative). Rendering those coordinates requires the floating element's containing block to be the viewport — in browsers that's automatic when ancestors aren't positioned (or via portal). Without containing-block resolution, every tooltip rendered inside nested/scrollable/static containers needs either a portal or a coordinate-space conversion that subtracts live ancestor origins.
@kommander mentioned in #406 being open to "make absolutely positioned elements relative to the closest explicitly relative positioned parent" — that's exactly the browser behavior this issue asks for.
Suggested direction
Two options, in increasing order of breakage:
Minimal (fix the explicit
staticcase only). opentui defaults_positionTypetorelative, andrelativewithout offsets behaves like CSSstaticin practice — so every implicit container today effectively acts as a containing block, and existing trees depend on that. The least-breaking change is: an explicitposition: "static"stops being a containing block, so its absolute children resolve against the next ancestor (ultimately the terminal root). Everything else keeps today's behavior. This gives a working escape hatch: mark intermediate containersstaticto get viewport-relative overlays, and it turns the currently-uselessstaticflag into a meaningful one.Full CSS semantics. Resolve absolute
left/topagainst the nearest ancestor with an explicitrelative/absoluteposition (falling back to the terminal root), i.e. offset the child's Yoga position by(parentOrigin − containingBlockOrigin)when applying layout. Existing parent-relative usage (sliders/switches withposition: "relative"tracks) keeps working since their containing block is already the immediate parent; trees relying on an implicit-relative parent as containing block would shift.
Related: there is no position: "fixed" (PositionTypeString is static | relative | absolute) — viewport-anchored overlays would be covered by the root fallback above, or by adding a fixed mode.
Source: anomalyco/opentui