Pointer
A custom cursor that tracks 1:1 within a bounded PointerArea. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add pointerpnpm dlx kosmesis add pointeryarn dlx kosmesis add pointerbunx kosmesis add pointerCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Prop, Ref, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface PointerAreaProps { class?: string; children?: Children;}/** Queries its own `[data-slot=pointer]` child directly on move rather than via a shared instance ref. */@Component()export class PointerArea extends StatefulComponent { @Prop() class?: string; @Prop() children?: Children; @Ref<HTMLDivElement>() containerRef!: RefType<HTMLDivElement>; private readonly _handlePointerMove = (event: PointerEvent) => { const container = this.containerRef.current; const pointer = container?.querySelector<HTMLElement>("[data-slot=pointer]"); if (!container || !pointer) return; const rect = container.getBoundingClientRect(); pointer.style.transform = `translate(${String(event.clientX - rect.left)}px, ${String(event.clientY - rect.top)}px)`; pointer.dataset.visible = ""; }; private readonly _handlePointerLeave = () => { const pointer = this.containerRef.current?.querySelector<HTMLElement>("[data-slot=pointer]"); if (pointer) delete pointer.dataset.visible; }; render() { return ( <div ref={this.containerRef} data-slot="pointer-area" class={cn("relative", this.class)} onPointerMove={this._handlePointerMove} onPointerLeave={this._handlePointerLeave} > {this.children} </div> ); }}export interface PointerProps { class?: string; children?: Children;}@Component()export class Pointer extends StatelessComponent<PointerProps> { render() { const { class: cls, children } = this.props; return ( <div data-slot="pointer" class={cn( "pointer-events-none absolute top-0 left-0 z-50 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-150", "data-[visible]:opacity-100", cls, )} > {children ?? <span class="block size-3 rounded-full bg-foreground" />} </div> ); }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Prop, Ref, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class PointerStyles extends Stylesheet { $area = this.css({ position: "relative" }); $pointer = this.css({ pointerEvents: "none", position: "absolute", top: 0, left: 0, zIndex: 50, transform: "translate(-50%, -50%)", opacity: 0, transition: "opacity 150ms ease", }).on("&[data-visible]", { opacity: 1 }); $dot = this.css({ display: "block", height: "0.75rem", width: "0.75rem", borderRadius: "9999px", backgroundColor: t.foreground });}export interface PointerAreaProps { class?: string; children?: Children;}/** Queries its own `[data-slot=pointer]` child directly on move rather than via a shared instance ref. */@Component()export class PointerArea extends StatefulComponent { @Styled(PointerStyles) $s!: PointerStyles; @Prop() class?: string; @Prop() children?: Children; @Ref<HTMLDivElement>() containerRef!: RefType<HTMLDivElement>; private readonly _handlePointerMove = (event: PointerEvent) => { const container = this.containerRef.current; const pointer = container?.querySelector<HTMLElement>("[data-slot=pointer]"); if (!container || !pointer) return; const rect = container.getBoundingClientRect(); pointer.style.transform = `translate(${String(event.clientX - rect.left)}px, ${String(event.clientY - rect.top)}px)`; pointer.dataset.visible = ""; }; private readonly _handlePointerLeave = () => { const pointer = this.containerRef.current?.querySelector<HTMLElement>("[data-slot=pointer]"); if (pointer) delete pointer.dataset.visible; }; render() { return ( <div ref={this.containerRef} data-slot="pointer-area" class={cx(this.$s.$area, this.class)} onPointerMove={this._handlePointerMove} onPointerLeave={this._handlePointerLeave} > {this.children} </div> ); }}export interface PointerProps { class?: string; children?: Children;}@Component()export class Pointer extends StatelessComponent<PointerProps> { @Styled(PointerStyles) $s!: PointerStyles; render() { const { class: cls, children } = this.props; return ( <div data-slot="pointer" class={cx(this.$s.$pointer, cls)}> {children ?? <span class={this.$s.$dot} />} </div> ); }}Examples
About
Purely presentational — no Morphos equivalent. Same imperative DOM-querying approach as Dock:
PointerArea's onPointerMove queries its own [data-slot=pointer] child directly and moves it,
rather than requiring a shared instance reference. Distinct from SmoothCursor (viewport-global,
eased motion): this tracks 1:1 within a single bounded container. Pair with cursor-none on the
area to hide the native cursor.
Usage
import { Pointer, PointerArea } from "@/components/ui/pointer";
<PointerArea class="relative flex h-56 cursor-none items-center justify-center">
Hover me
<Pointer />
</PointerArea>Props
PointerArea and Pointer both accept class and children only — Pointer's children
override the default dot indicator.