Skip to main content

Layout and positioning

Layout itself is yoga's job — see styling. The two packages here cover what yoga does not: placing a popup relative to a trigger across screen edges, and rendering a long list without a node per row. Both are the core half of a library whose React wrapper is DOM-bound; the core is published separately so non-DOM hosts can use it.

@floating-ui/core​

Adapter, ~35 lines (a platform object). @floating-ui/core@1.8.0.

Floating UI's positioning engine without DOM code. Hand computePosition a platform object — getElementRects, getDimensions, getClippingRect, isRTL — and get the full middleware pipeline (offset, flip, shift, size, arrow, autoPlacement, hide) and its placement vocabulary.

As in the built-in anchorRect(), a drawn node's node.abs rect plus the window's screen origin is the reference rect in screen coordinates, screenOf(node) gives the clipping rect, and the popup-to-be is just {width, height}. The result's {x, y, placement} drops straight into <popup x y>.

import { computePosition, offset, flip, shift } from '@floating-ui/core';

// The screen a node is on. Same three lines the built-in anchorRect uses;
// it is not exported from the package, so keep a copy.
function screenOf(node) {
const app = node?.app;
const screen = (app?.display ?? app?.X?.display)?.screen?.[0];
return screen?.pixel_width ? screen : null;
}

// reference = a drawn node (post-layout); floating = the popup's {width, height}
function screenRectOf(node) {
const win = node.root?.window;
const origin = win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
return {
x: origin.x + node.abs.x,
y: origin.y + node.abs.y,
width: node.abs.width,
height: node.abs.height,
};
}

const platform = {
getElementRects: ({ reference, floating }) => ({
reference: screenRectOf(reference),
floating: { x: 0, y: 0, width: floating.width, height: floating.height },
}),
getDimensions: (el) => el.abs ?? el, // node or plain {width, height}
getClippingRect: ({ element }) => {
const s = screenOf(element?.node ?? element);
return s
? { x: 0, y: 0, width: s.pixel_width, height: s.pixel_height }
: { x: 0, y: 0, width: Infinity, height: Infinity };
},
isElement: () => true,
isRTL: () => false,
};

const { x, y, placement } = await computePosition(
triggerNode, // from a ref on the trigger <box>
{ width: 200, height: 150, node: triggerNode },
{
placement: 'bottom-start',
middleware: [offset(4), flip(), shift({ padding: 8 })],
platform,
},
);
// -> <popup x={x} y={y}> ; placement tells you which side won

Over anchorRect this adds middleware composition (shift with padding, size to clamp long menus, arrow) and alignment fallback — flip with crossAxis: 'alignment' switches bottom-start to bottom-end when that fits. anchorRect's placement, align and offset options map 1:1 onto placement plus the offset() middleware.

  • computePosition is async — await it, though this platform is synchronous.
  • getClippingRect above is screen 0's full rect, the same single-monitor simplification as screenOf. On a dual-head X display, popups clamp to the bounding box of both monitors.
  • @floating-ui/dom and @floating-ui/react are not usable (DOM platform and DOM interactions). Only core and its @floating-ui/utils dependency belong in an X11 app.
  • Safe-polygon hover lives in @floating-ui/react's DOM-bound useHover, not in core; keep react-x11's built-in one.

@tanstack/virtual-core​

Adapter, ~40 lines. @tanstack/virtual-core@3.17.7.

The engine behind useVirtualizer: from a count, an estimateSize, the viewport rect and the scroll offset it computes the visible items (getVirtualItems()), the total size (getTotalSize()), and offsets for scrollToIndex. Every DOM touchpoint is a constructor option.

Do not import @tanstack/react-virtual. Its first line is import { flushSync } from "react-dom": without react-dom it fails to resolve; with it, it silently flushes into the wrong reconciler (silent failures). The hook below is everything the React wrapper adds.

Three seams wire to a scrolling <box>:

  • observeElementRect → <box onViewport>, which fires from layout with {width, height, contentWidth, contentHeight} — including before any scroll, which is what a list needs on mount;
  • observeElementOffset → <box onScroll> (scrollY);
  • scrollToFn → ref.scrollTo(offset).

getScrollElement() must return something with scrollHeight/clientHeight (virtual-core duck-types Element vs Window with 'scrollHeight' in el); a getter shim over the node's contentHeight/abs.height satisfies it.

import React from 'react';
import { Virtualizer } from '@tanstack/virtual-core';

function useX11Virtualizer({ count, estimateSize, overscan = 2 }) {
const ref = React.useRef(null);
const obs = React.useRef({});
const [, rerender] = React.useReducer((n) => n + 1, 0);
const [element] = React.useState(() => ({
// element-shaped view of the node
get scrollHeight() {
return ref.current?.contentHeight ?? 0;
},
get clientHeight() {
return ref.current?.abs.height ?? 0;
},
get scrollWidth() {
return ref.current?.contentWidth ?? 0;
},
get clientWidth() {
return ref.current?.abs.width ?? 0;
},
}));
const [v] = React.useState(
() =>
new Virtualizer({
count,
estimateSize,
overscan,
getScrollElement: () => (ref.current ? element : null),
observeElementRect: (_i, cb) => {
obs.current.rect = cb;
},
observeElementOffset: (_i, cb) => {
obs.current.offset = cb;
cb(0, false);
},
scrollToFn: (offset) => ref.current?.scrollTo(offset),
onChange: () => rerender(),
}),
);
v.setOptions({ ...v.options, count, onChange: () => rerender() });
React.useEffect(() => v._didMount(), []);
React.useEffect(() => v._willUpdate());
return {
virtualizer: v,
scrollProps: {
ref,
onViewport: (r) => obs.current.rect?.(r),
onScroll: (s) => obs.current.offset?.(s.scrollY, true),
},
};
}
function BigList() {
const { virtualizer, scrollProps } = useX11Virtualizer({
count: 10000,
estimateSize: () => 24,
});
return (
<box {...scrollProps} style={{ flexGrow: 1, overflow: 'scroll' }}>
<box style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((item) => (
<text
key={item.key}
style={{ position: 'absolute', top: item.start, height: item.size }}
>
{`row ${item.index}`}
</text>
))}
</box>
</box>
);
}
  • First paint shows zero rows. Layout runs on the frame clock after the mount commit, onViewport reports a tick later, and the list fills in on the next frame. Fine interactively; tests must tick a few times.
  • estimateSize is the only measurement; measureElement is built on ResizeObserver and getBoundingClientRect. For variable-height rows pass exact sizes: estimateSize: (i) => heights[i].
  • _didMount/_willUpdate are underscored but are the documented pattern for custom adapters — the official React, Vue and Svelte adapters call exactly these.