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.
How It Works
- 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);
}
#![allow(unused)]
fn main() {
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.
CSS Properties
These properties configure pointer tracking on an element. Setting pointer-space activates the system for that element.
pointer-space
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; }
pointer-origin
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; }
pointer-range
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
pointer-smoothing
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.
Environment Variables
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) |
Using pointer-inside as a Gate
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.
Calc Functions
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 |
mix — Linear Interpolation
/* 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);
smoothstep — Smooth Transitions
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)));
Units in calc()
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);
Examples
3D Tilt Card
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);
}
Hover Reveal
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)));
}
Distance-Based Effects
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 Glow
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))));
}
Subtle Rotation
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)));
}
Pressure Response
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)));
}
Combined Effects
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);
}
How It Works Internally
-
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.
Tips
- 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.