Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Set pointer-space on an element in CSS to enable tracking.
  2. Each frame, the system computes the pointer’s normalized position relative to that element.
  3. Results are exposed as env() variables usable in any calc() expression.
  4. 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.

ValueDescription
selfPosition relative to the element’s own bounds (default)
parentPosition relative to the parent element
viewportPosition relative to the viewport
#card { pointer-space: self; }

pointer-origin

The origin point for coordinate normalization.

ValueDescription
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:

VariableTypeDescription
env(pointer-x)floatNormalized X position in configured range
env(pointer-y)floatNormalized Y position in configured range
env(pointer-vx)floatX velocity (normalized units/second)
env(pointer-vy)floatY velocity (normalized units/second)
env(pointer-speed)floatTotal speed: sqrt(vx² + vy²)
env(pointer-distance)floatDistance from origin (normalized units)
env(pointer-angle)floatAngle from origin (radians, 0 = right, pi/2 = up)
env(pointer-inside)0.0/1.01.0 if cursor is inside element, 0.0 otherwise (smoothed)
env(pointer-active)0.0/1.01.0 if mouse button is pressed while over element
env(pointer-pressure)floatTouch/click pressure (0.0-1.0). Mouse: binary 0/1. Touch: hardware pressure (smoothed)
env(pointer-touch-count)floatNumber of active touch points (0 for mouse input)
env(pointer-hover-duration)floatSeconds 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:

FunctionSignatureDescription
mixmix(a, b, t)Linear interpolation: a + (b - a) * t
smoothstepsmoothstep(edge0, edge1, x)Hermite interpolation (smooth 0-1 curve)
stepstep(edge, x)0 if x < edge, 1 otherwise
clampclamp(min, val, max)Clamp value to range
remapremap(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

  1. Registration: When the CSS parser encounters pointer-space on an element, it stores a PointerSpaceConfig on the ElementStyle. During stylesheet application, elements with this config are registered in PointerQueryState.

  2. 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.

  3. Env resolution: When a calc() expression containing env(pointer-*) is evaluated (for opacity, border-radius, rotate, etc.), it resolves against the element’s ElementPointerState.

  4. 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-smoothing for visual properties — even a small value like 0.06 eliminates jitter and creates a polished feel.
  • Gate with pointer-inside to prevent effects from firing when the cursor is far away. Multiply: env(pointer-x) * env(pointer-inside).
  • Use smoothstep for distance effects — raw pointer-distance drops off linearly, but smoothstep creates 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-space set are tracked. No per-frame cost for elements that don’t opt in.