A textarea's bounding box is easy to measure. An autocomplete menu usually needs a different anchor: the insertion point inside it.
Those positions diverge as soon as text wraps or the textarea scrolls. A menu anchored to the bottom of the element can be nowhere near the word the user is editing.
I maintain Caret Geometry, an alpha-stage browser primitive for measuring that anchor. Here is how its boundary fits into a popup implementation.
Start with the coordinate you actually need
The smallest call looks like this:
import { getCaretRect } from "@nipe-solutions/caret-geometry";
const rect = getCaretRect(textarea);
if (rect) {
console.log(rect.left, rect.top, rect.height);
}
The rectangle uses viewport-relative CSS pixels in the target's browsing context, matching the coordinate space of getBoundingClientRect(). It is not a document offset or a device-pixel measurement.
For supported targets, a missing current layout or relevant selection can produce null. Unsupported input types throw an error. Password inputs are deliberately unsupported and are rejected before their value is mirrored.
A caret rectangle answers where the anchor is. Choosing which side of it a popup should occupy is another job.
There are two reasons to reposition
A user can move the caret while the textarea stays completely still. Arrow keys, a selection change, composition and internal scrolling can all matter.
The reverse happens too: the caret stays at the same text position while a parent scrolls, the page resizes or the surrounding layout shifts.
The integration needs to respond to both:
| What changed? | Who observes it? |
|---|---|
| Caret, selection, composition or internal textarea scroll | observeCaretGeometry |
| Surrounding scroll, resize or layout changes | The positioning engine's update mechanism |
Both trigger the same positioning function. Attaching a listener only to typing misses part of the problem.
Connect a virtual caret to Floating UI
This example follows the compiling integration example in the repository. The popup should already be mounted and styled with position: fixed.
import {
createCaretVirtualElement,
observeCaretGeometry,
} from "@nipe-solutions/caret-geometry";
import {
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "@floating-ui/dom";
export function connectCaretPopup(
textarea: HTMLTextAreaElement,
popup: HTMLElement,
): (() => void) | null {
const caret = createCaretVirtualElement(textarea);
if (!caret) return null;
const updatePosition = async () => {
if (!caret.getCaretRect()) {
popup.hidden = true;
return;
}
popup.hidden = false;
const { x, y } = await computePosition(caret, popup, {
strategy: "fixed",
placement: "bottom-start",
middleware: [offset(6), flip(), shift({ padding: 8 })],
});
Object.assign(popup.style, {
left: `${x}px`,
top: `${y}px`,
});
};
const caretObserver = observeCaretGeometry(
textarea,
updatePosition,
);
const stopLayout = autoUpdate(caret, popup, updatePosition);
void updatePosition();
return () => {
caretObserver.disconnect();
stopLayout();
};
}
Keep the returned cleanup function and call it when that connection ends, before removing the elements. The integration guide explains the boundary in more detail.
Caret Geometry supplies the virtual reference and caret updates. Floating UI handles the popup's placement, including the offset, flip and shift choices above.
Your application still owns when suggestions open, which results to show, keyboard selection, focus and accessibility semantics. A measured caret does not implement a combobox.
Check more than typing at the end of a line
Before relying on a popup, I would try:
- Long wrapped text and internal textarea scrolling.
- Moving the caret with the keyboard without typing.
- A backward selection. The default endpoint is its focus; in a backward input selection, that means
selectionStart. - IME composition in the browsers you support.
- Scrolling an ancestor and resizing the page with the popup open.
If you pass an explicit text-control position, it uses a UTF-16 string index. That matters when your application computes positions around emoji or other text represented by multiple code units.
This library is still alpha. The compatibility documentation and release-readiness notes describe its current scope and remaining gates.
I'm looking for real integrations, especially autocomplete, mention menus and editor helpers. If the anchor drifts in your layout, a small reproduction with the browser and target type is useful feedback.
What is the most awkward caret-positioning case in your editor?
Caret Geometry is part of my NIPE open-source frontend libraries.
Top comments (0)