#1512·opentui

Absolute positioning resolves against immediate parent, not the nearest positioned ancestor (CSS containing-block semantics)

Author: yuyi919Created Sep 16, 2026Updated Sep 16, 2026

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

typescript
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:

  1. Minimal (fix the explicit static case only). opentui defaults _positionType to relative, and relative without offsets behaves like CSS static in practice — so every implicit container today effectively acts as a containing block, and existing trees depend on that. The least-breaking change is: an explicit position: "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 containers static to get viewport-relative overlays, and it turns the currently-useless static flag into a meaningful one.

  2. Full CSS semantics. Resolve absolute left/top against the nearest ancestor with an explicit relative/absolute position (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 with position: "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.