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:
<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 property | Default | Meaning |
|---|---|---|
--calmui-follow-offset-x | 18px | Horizontal distance from the pointer to the box. |
--calmui-follow-offset-y | 18px | Vertical distance from the pointer to the box. |
--calmui-follow-edge | 8px | Margin 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.
Related
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.