CalmUI
Components

Follow the Pointer

Anchor a tooltip, cursor hint or drag ghost to the mouse without publishing its position through the Store.

A cursor tooltip has to sit next to the mouse and stay there. The obvious way to build one, having the game write the pointer position into a Store field and bind it to an inline left/top, is the wrong way: every mouse move becomes a Store update, a document reload, a relayout and a style resolution. The cost forces a throttle, and the throttle is what makes the tooltip jump between positions instead of following.

Mark the element instead. The engine anchors it to the pointer every rendered frame, as a transform on the node it already paints:

CursorHint.calmui
<template>
  <aside class="cursor-hint" data-calmui-follow-pointer="true">
    <strong>{{ title }}</strong>
    <p>{{ body }}</p>
  </aside>
</template>

<style scoped>
.cursor-hint {
  position: absolute;
  pointer-events: none;
  --calmui-follow-offset-x: 18px;
  --calmui-follow-offset-y: 22px;
}
</style>

The game publishes only what the tooltip says (visible, title, body). It never publishes a position.

Requirements

The element must be position: absolute or position: fixed. A followed element is placed, not laid out, so it must be out of normal flow, otherwise it would drag its siblings around as it moves.

Placement

The element's top-left is put at the pointer plus an offset. If that would push the box past the right or bottom edge of the viewport, that axis mirrors to the other side of the pointer (pointer − offset − size), so the hint flips instead of being clipped. The two axes flip independently. The result is then clamped inside the viewport, so a box that fits nowhere still starts on screen rather than ending off it.

Custom propertyDefaultMeaning
--calmui-follow-offset-x18pxHorizontal distance from the pointer to the box.
--calmui-follow-offset-y18pxVertical distance from the pointer to the box.
--calmui-follow-edge8pxMargin kept free at the viewport edge before the box flips.

Declare them wherever you would declare any other property: a workspace stylesheet listed in calmui.config.json, a component's <style scoped>, a :root token, or an inline style. The engine reads the element's resolved value, so the cascade applies as usual, and var() in the value is folded at build time like anywhere else. Use a plain length: a value the engine cannot read as pixels (a percentage, a calc()) falls back to the default rather than being misread. Negative offsets are allowed and centre the box on the cursor if you want that.

These three names are the only custom properties the compiler keeps in the shipped stylesheet without a var() reference; ordinary design tokens are still folded into their use sites and pruned.

Nested followers

Descendants of a following element move with it. A descendant that opts in itself (data-calmui-follow-pointer="true" with its own offsets) is placed at its own origin instead: the cursor tooltip's dot is a child of the bubble with --calmui-follow-offset-x:-7px; --calmui-follow-offset-y:-7px; --calmui-follow-edge:0px, so it stays centred on the cursor while the bubble mirrors to the other side of the pointer near a viewport edge.

Opting out

Only the literal string true turns following on, so you can bind the attribute and keep one component for both a cursor-anchored and a world-anchored prompt:

<aside class="hint" :data-calmui-follow-pointer="pinned ? 'false' : 'true'">

With the attribute absent, or any value other than true, the element stays exactly where CSS put it.

What it costs

Nothing per frame beyond a transform. Following never triggers a layout, a style resolution or a document reload; it composes onto the same per-node transform the renderer already applies, so the element's own transitions and keyframe animations keep working underneath it. A tooltip that fades and scales in still fades and scales in while it follows the cursor.

Hit testing

A followed element and everything inside it are removed from hit testing. They sit under the cursor by construction, so letting them hit test would take hover away from whatever opened them: the tooltip would fight the element it describes and flicker. This holds regardless of pointer-events; setting pointer-events: none as well is good practice but the engine does not rely on it.

Without a pointer

On a gamepad, or before the mouse has moved for the first time, no pointer position is known and no transform is applied: the element renders where its CSS puts it. Give a followed element a sensible static position for that case.

Hover state is re-evaluated whenever the document changes, not only when the mouse moves. An element that appears or moves under a resting cursor receives pointerenter, and one that moves out from under it receives pointerleave, with no mouse movement required, so a hint bound to @pointerenter opens even while the element underneath is re-rendering (a HUD chip whose timer text ticks every second). See Events.

On this page