-
-
Notifications
You must be signed in to change notification settings - Fork 10
Pointer Query
The pointer query system exposes continuous cursor position, velocity, distance, and angle as CSS environment variables on any element. This lets you build pointer-reactive effects — 3D tilt, hover reveals, distance-based glow, dynamic corners — entirely in CSS, with no Rust event handlers.
- Set
pointer-spaceon an element in CSS to enable tracking. - Each frame, the system computes the pointer's normalized position relative to that element.
- Results are exposed as
env()variables usable in anycalc()expression. - Any numerical CSS property can read these values: opacity, border-radius, rotate, border-width, perspective transforms, and more.
#card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.08;
/* 3D tilt follows cursor */
perspective: 800px;
rotate-y: calc(env(pointer-x) * env(pointer-inside) * 25deg);
rotate-x: calc(env(pointer-y) * env(pointer-inside) * -25deg);
}div()
.id("card")
.class("my-card")
.w(300.0)
.h(200.0)
.child(text("Hover me"))No event handlers, no state management — the CSS drives everything.
These properties configure pointer tracking on an element. Setting pointer-space activates the system for that element.
The coordinate space for pointer position computation.
| Value | Description |
|---|---|
self |
Position relative to the element's own bounds (default) |
parent |
Position relative to the parent element |
viewport |
Position relative to the viewport |
#card { pointer-space: self; }The origin point for coordinate normalization.
| Value | Description |
|---|---|
center |
(0,0) at element center, extends symmetrically (default) |
top-left |
(0,0) at top-left corner |
bottom-left |
(0,0) at bottom-left, Y-up (shader coordinates) |
#card { pointer-origin: center; }The output range for normalized coordinates. Takes two floats: min and max.
/* Default: symmetric -1 to 1 (good for center origin) */
#card { pointer-range: -1.0 1.0; }
/* 0 to 1 (good for top-left origin) */
#card { pointer-range: 0.0 1.0; }With center origin and -1.0 1.0 range:
- Cursor at element center:
pointer-x = 0,pointer-y = 0 - Cursor at left edge:
pointer-x = -1 - Cursor at right edge:
pointer-x = 1
Exponential smoothing time constant in seconds. Smooths position, velocity, and the pointer-inside flag for gradual transitions.
/* No smoothing — instant tracking */
#card { pointer-smoothing: 0; }
/* Subtle lag — responsive but smooth */
#card { pointer-smoothing: 0.08; }
/* Heavy smoothing — slow, floaty feel */
#card { pointer-smoothing: 0.2; }When the cursor leaves the element, smoothed values decay toward the origin (0,0) instead of snapping. This creates a natural fade-out effect.
Once pointer-space is set on an element, these env() variables resolve inside any calc() expression on that element:
| Variable | Type | Description |
|---|---|---|
env(pointer-x) |
float | Normalized X position in configured range |
env(pointer-y) |
float | Normalized Y position in configured range |
env(pointer-vx) |
float | X velocity (normalized units/second) |
env(pointer-vy) |
float | Y velocity (normalized units/second) |
env(pointer-speed) |
float | Total speed: sqrt(vx² + vy²)
|
env(pointer-distance) |
float | Distance from origin (normalized units) |
env(pointer-angle) |
float | Angle from origin (radians, 0 = right, pi/2 = up) |
env(pointer-inside) |
0.0/1.0 | 1.0 if cursor is inside element, 0.0 otherwise (smoothed) |
env(pointer-active) |
0.0/1.0 | 1.0 if mouse button is pressed while over element |
env(pointer-pressure) |
float | Touch/click pressure (0.0-1.0). Mouse: binary 0/1. Touch: hardware pressure (smoothed) |
env(pointer-touch-count) |
float | Number of active touch points (0 for mouse input) |
env(pointer-hover-duration) |
float | Seconds since cursor entered (0 if outside) |
Multiply by env(pointer-inside) to make effects only appear on hover:
/* Rotation ONLY when hovered */
rotate: calc(env(pointer-x) * env(pointer-inside) * 5deg);
/* Opacity: 0.3 normally, 1.0 on hover */
opacity: calc(mix(0.3, 1.0, env(pointer-inside)));Because pointer-inside is smoothed, the transition in/out is gradual when pointer-smoothing is set.
These functions work inside calc() and are especially useful with pointer variables:
| Function | Signature | Description |
|---|---|---|
mix |
mix(a, b, t) |
Linear interpolation: a + (b - a) * t
|
smoothstep |
smoothstep(edge0, edge1, x) |
Hermite interpolation (smooth 0-1 curve) |
step |
step(edge, x) |
0 if x < edge, 1 otherwise |
clamp |
clamp(min, val, max) |
Clamp value to range |
remap |
remap(val, in_lo, in_hi, out_lo, out_hi) |
Remap from one range to another |
/* Opacity: 30% when far, 100% when hovering */
opacity: calc(mix(0.3, 1.0, env(pointer-inside)));
/* Border-radius: 4px far, 48px near */
border-radius: calc(mix(4, 48, smoothstep(1.4, 0.0, env(pointer-distance))) * 1px);Creates an S-curve between two edge values. When edge0 > edge1, the curve is inverted (1 at close range, 0 at far range).
/* Opacity fades in as pointer approaches (inverted smoothstep) */
opacity: calc(smoothstep(1.8, 0.0, env(pointer-distance)));Pointer env variables are unitless floats. To produce a CSS value with units, multiply by a unit literal:
/* 1px unit applied after the math */
border-radius: calc(mix(4, 48, env(pointer-inside)) * 1px);
border-width: calc(mix(0, 4, env(pointer-inside)) * 1px);
/* Degrees for rotation */
rotate-y: calc(env(pointer-x) * 25deg);Perspective rotate-x/y follow the cursor for a true 3D card effect.
#tilt-card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.08;
border-radius: 16px;
background: #1e2438;
perspective: 800px;
rotate-y: calc(env(pointer-x) * env(pointer-inside) * 25deg);
rotate-x: calc(env(pointer-y) * env(pointer-inside) * -25deg);
}Element fades from dim to full brightness on hover.
#reveal-card {
pointer-space: self;
pointer-smoothing: 0.12;
background: #2a1a3e;
opacity: calc(mix(0.3, 1.0, env(pointer-inside)));
}Opacity, corners, or borders that respond to how close the cursor is to the element's center.
#distance-card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.06;
/* Opacity increases as pointer approaches center */
opacity: calc(smoothstep(1.8, 0.0, env(pointer-distance)));
}
#corners-card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.08;
/* Corners round as pointer approaches */
border-radius: calc(mix(4, 48, smoothstep(1.4, 0.0, env(pointer-distance))) * 1px);
}Border grows and appears as the cursor approaches.
#border-card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.06;
border-radius: 16px;
border-color: #4488cc;
border-width: calc(mix(0, 4, smoothstep(1.4, 0.0, env(pointer-distance))) * 1px);
opacity: calc(mix(0.3, 1.0, smoothstep(1.8, 0.0, env(pointer-distance))));
}Card rotates gently following cursor x-position.
#rotate-card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.1;
rotate: calc(env(pointer-x) * env(pointer-inside) * 5deg);
opacity: calc(mix(0.5, 1.0, env(pointer-inside)));
}Scale and opacity respond to touch pressure or click state. On desktop, mouse clicks produce a binary 0→1 pressure that smooths naturally via pointer-smoothing. On mobile devices with 3D Touch or pressure-sensitive screens, the response is continuous.
#pressure-card {
pointer-space: self;
pointer-smoothing: 0.06;
/* Scale up slightly when pressed, proportional to pressure */
scale: calc(1.0 + env(pointer-pressure) * 0.1);
/* Full opacity when pressed hard */
opacity: calc(mix(0.4, 1.0, env(pointer-pressure)));
}Multiple properties respond simultaneously for rich interactive cards.
#combo-card {
pointer-space: self;
pointer-origin: center;
pointer-range: -1.0 1.0;
pointer-smoothing: 0.08;
border-radius: calc(mix(8, 40, smoothstep(1.4, 0.0, env(pointer-distance))) * 1px);
border-width: calc(mix(0, 3, smoothstep(1.2, 0.0, env(pointer-distance))) * 1px);
border-color: #cc66aa;
opacity: calc(smoothstep(1.6, 0.0, env(pointer-distance)));
rotate: calc(env(pointer-x) * env(pointer-inside) * 3deg);
}-
Registration: When the CSS parser encounters
pointer-spaceon an element, it stores aPointerSpaceConfigon theElementStyle. During stylesheet application, elements with this config are registered inPointerQueryState. -
Per-frame update: Each frame,
PointerQueryState::update()runs for all tracked elements. It uses the event router's hit test results to determine hover state and element bounds, then computes normalized coordinates, velocity, distance, and angle. -
Env resolution: When a
calc()expression containingenv(pointer-*)is evaluated (for opacity, border-radius, rotate, etc.), it resolves against the element'sElementPointerState. -
Continuous redraw: While any pointer-tracked element is hovered (or smoothing is active), the system requests redraws to keep values updating.
State is keyed by element string ID (not LayoutNodeId), so it persists across tree rebuilds. Smoothed values carry over seamlessly.
-
Always use
pointer-smoothingfor visual properties — even a small value like0.06eliminates jitter and creates a polished feel. -
Gate with
pointer-insideto prevent effects from firing when the cursor is far away. Multiply:env(pointer-x) * env(pointer-inside). -
Use
smoothstepfor distance effects — rawpointer-distancedrops off linearly, butsmoothstepcreates a natural proximity gradient. - Combine freely — all env variables are independent. Mix position-based rotation with distance-based opacity and hover-gated borders in the same element.
-
Performance: Only elements with
pointer-spaceset are tracked. No per-frame cost for elements that don't opt in.
Getting Started
Mobile Development
Core Concepts
Animation
Components
Component Library (blinc_cn)
Widgets
Advanced
Architecture