Place one tp-cursor anywhere; it occupies no space and draws in the top layer, so dialogs and menus never cover it. With the default scope="document" it replaces the pointer on the whole page. Every example here sets scope="region", which limits following to the cursor’s parent element (or the element whose id is in region), so the rest of the page keeps the native pointer.
The cursor is decorative: its surface is hidden from assistive technology and never a hit target, so focus order and activation are unchanged. Touch and coarse pointers never engage it, and under reduced motion it snaps to the pointer instead of trailing. Use it for portfolios, galleries and landing pages, not for dense application screens.
Following
The spring is tuned with stiffness, damping and mass. Lower stiffness lags further behind; lower damping overshoots and settles. The frame loop stops once the cursor rests and starts again when the pointer moves.
Spring
Default density
Compact density
Restart from the initial state
Show the copyable source
Wrap content in tp-cursor-zone to change the cursor while the pointer is inside it. A zone is layout-neutral (display: contents), so wrapping never changes the content’s box. for addresses one cursor by id; without it, a zone addresses every cursor on the page.
state is published on the cursor as data-cursor-state and reported with tp-cursor-state-change.
--tp-cursor-size, --tp-cursor-color, --tp-cursor-blend, --tp-cursor-opacity and --tp-cursor-radius set on the zone are adopted while it is active.
A <template> child replaces the cursor’s content, the default ring included, with a label, badge or icon. Keep it non-interactive.
Zones
Default density
Compact density
Restart from the initial state
Show the copyable source
--tp-cursor-blend: difference inverts the cursor over what is beneath it, so a white cursor stays visible on light and dark areas alike. Nested zones work; the innermost wins. Zones inside shadow roots work too. While the page or any scroll container scrolls under a still pointer, including one inside a shadow root such as Scroll area, the zones under the pointer are resolved again in every frame, so the state never waits for the browser’s own hover update at the end of the scroll; refresh() does the same on demand.
Sticking
stick="center" pulls the cursor to the middle of the zone through the same spring. stick="bounds" also morphs the default ring into the zone’s rectangle. Zones can carry their own stiffness, damping and mass while active.
Sticking
Default density
Compact density
Restart from the initial state
Show the copyable source
Bounds come from the zone’s element children, so a zone that wraps only text sticks to nothing.
Custom look and several cursors
Slot any content into tp-cursor to replace the default ring. Several cursors can share a region with their own physics, such as a stiff dot and a lagging ring, and a zone with for reacts on one of them only.
Dot and ring
Default density
Compact density
Restart from the initial state
Show the copyable source
Put native on a zone around text entry, embedded documents, players or anything else that needs the real pointer. Inside it the native pointer returns and the cursor hides.
Pointer lock is not supported. No events arrive while the pointer is over an embedded document, so the cursor rests at the last point until the pointer leaves or moves. Hiding the host pointer reaches library shadow parts; third-party shadow roots that set their own cursor keep it, and the custom cursor draws on top. The cursor is not printed.
Native passthrough
Default density
Compact density
Restart from the initial state
Show the copyable source
Send inquiry
Send inquiry
<div style="display:grid;gap:var(--tp-space-4);padding:var(--tp-space-6);border:var(--tp-border-width) dashed var(--tp-border);border-radius:var(--tp-radius-xl)">
<tp-cursor id="cursor-native" scope="region"></tp-cursor>
<tp-cursor-zone for="cursor-native" native>
<tp-field label="Message" description="The native text cursor returns over the field.">
<tp-text-area rows="3" placeholder="Tell us about your project"></tp-text-area>
</tp-field>
</tp-cursor-zone>
<tp-cursor-zone for="cursor-native" stick="bounds"><tp-button style="justify-self:start">Send inquiry</tp-button></tp-cursor-zone>
</div>
Programmatic state
setState() sets a state that persists until it is cleared with null or a zone takes over. Style each state from CSS through the data-cursor-state marker on the host.
Programmatic state
Default density
Compact density
Restart from the initial state
Show the copyable source
tp-cursor-state-change fires before every change with its reason, hover or programmatic, and can be canceled to keep the current state.
Reduced motion
Under prefers-reduced-motion, or with motion-policy="reduce", the cursor stays but follows the pointer instantly and zone transitions are immediate. hide-native="false" keeps the native pointer under the cursor, for an overlay look.
Reduced motion
Default density
Compact density
Restart from the initial state
Show the copyable source
The state change is a non-blocking state role named state on the surface. A listener for tp-motion-request can claim it with respondWith to morph the content with an animation library; the default presentation transitions size, radius, border color, opacity and scale. The per-frame follow is pointer-coupled positioning and is never offered to drivers.
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionstiffnessstiffnessnumber—Spring stiffness, 1 through 1000.dampingdampingnumber—Damping coefficient, 0 through 100.massmassnumber—Mass, 0.1 through 10.rest-speedrestSpeednumber—Speed in pixels per second below which the follower may settle.rest-deltarestDeltanumber—Distance in pixels below which the follower may settle.scopescopeCursorScope'document'document follows everywhere; region only inside the region element.regionregionstring''Region element id for scope="region"; the parent element when empty.hide-nativehideNativebooleantrueHide the host pointer while following; false keeps it (overlay mode).
Properties
PropertyTypeDefaultDescriptionfollowingread-onlyboolean—Whether a mouse-like pointer is being followed.stateread-onlystring | null—The published state: the programmatic state, else the active zone's.zoneread-onlyTpCursorZone | null—The active zone.positionread-onlyCursorPoint—The follower's position in client pixels.velocityread-onlyCursorPoint—The follower's velocity in pixels per second.pointerread-onlyCursorPoint | null—The last pointer point inside the scope.pressedread-onlyboolean—Whether the followed pointer is pressed.stuckread-onlyboolean—Whether the cursor is attracted to a zone.
Events
EventTypeDescriptiontp-cursor-state-changeCustomEvent<CursorStateChangeDetail>Cancelable { value, previousValue, zone, reason, sourceEvent } before the state changes (hover for zones, programmatic for setState()).tp-cursor-follow-changeCustomEvent<CursorFollowChangeDetail>{ value, previousValue, reason, sourceEvent } when the cursor starts or stops following (pointer, disabled, programmatic).tp-motion-requestTpMotionRequestEventRole state on the surface with state, previousState, zone and stuck context.
Methods
MethodReturnsDescriptionaccepts(zone)booleanA region-scoped cursor takes only the zones inside its region.setState(name)voidSets a state that persists until cleared or replaced by a later zone entry.refresh()voidRe-resolves the zones under the pointer, re-measures attraction and re-enters the top layer.scrolled()voidContent scrolled under the pointer; a stuck cursor follows its zone.raise()voidRe-enters the top layer, above surfaces such as dialogs opened since the cursor appeared.
Slots
SlotDescriptiondefaultResting content; without it the content part paints the default ring.
CSS parts
PartDescription::part(surface)The fixed, pointer-transparent box in the top layer.::part(content)The resting content, the default ring or the active zone's content.
CSS custom properties
PropertyDefaultDescription--tp-cursor-size—Default ring size. Default var(--tp-space-6).--tp-cursor-color—Ring and content color. Default var(--tp-foreground).--tp-cursor-blend—Surface mix-blend-mode. Default normal.--tp-cursor-opacity—Surface opacity while shown. Default 1.--tp-cursor-radius—Default ring radius. Default var(--tp-radius-full).--tp-cursor-x—Published follower x in pixels (read-only).--tp-cursor-y—Published follower y in pixels (read-only).--tp-cursor-velocity-x—Published velocity in pixels per second (read-only).--tp-cursor-velocity-y—Published velocity in pixels per second (read-only).--tp-cursor-stuck-width—Published width of the zone the cursor is stuck to.--tp-cursor-stuck-height—Published height of the zone the cursor is stuck to.
<tp-cursor-zone>
Attributes
AttributePropertyTypeDefaultDescriptionstatestatestring''State name the cursor publishes while this zone is active.forforstring''Id of the one cursor this zone addresses; every cursor of the document when empty.nativenativebooleanfalseRestore the host pointer inside the zone.stickstickCursorStick'none'Pull the cursor to the zone's center, or morph it to the zone's bounds.stiffnessstiffnessnumber | nullnullThe cursor's stiffness while the zone is active; null keeps the cursor's value.dampingdampingnumber | nullnullThe cursor's damping while the zone is active; null keeps the cursor's value.massmassnumber | nullnullThe cursor's mass while the zone is active; null keeps the cursor's value.
Properties
PropertyTypeDefaultDescriptionhoveredread-onlyboolean—Whether the pointer is inside the zone.templateread-onlyHTMLTemplateElement | null—The first template child: content the cursor shows while the zone is active.cursorread-onlyElement | null—The cursor this zone addresses with for, or null when unresolved.