Components
Smooth Cursor
A viewport-global custom cursor that eases toward the pointer. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add smooth-cursorpnpm dlx kosmesis add smooth-cursoryarn dlx kosmesis add smooth-cursorbunx kosmesis add smooth-cursorCopy and paste the following code into your project.
import { StatefulComponent } 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 SmoothCursorProps { size?: number; ease?: number; class?: string; children?: Children;}/** Writes `transform` directly rather than through `@State`, since the rAF lerp loop needs to keep ticking every frame regardless of pointer movement. */@Component()export class SmoothCursor extends StatefulComponent { @Prop() size = 24; @Prop() ease = 0.15; @Prop() class?: string; @Prop() children?: Children; @Ref<HTMLDivElement>() elRef!: RefType<HTMLDivElement>; private _target = { x: 0, y: 0 }; private readonly _current = { x: 0, y: 0 }; private _frame = 0; private readonly _handlePointerMove = (event: PointerEvent) => { this._target = { x: event.clientX, y: event.clientY }; }; private readonly _tick = () => { this._current.x += (this._target.x - this._current.x) * this.ease; this._current.y += (this._target.y - this._current.y) * this.ease; const el = this.elRef.current; if (el) el.style.transform = `translate(${String(this._current.x)}px, ${String(this._current.y)}px) translate(-50%, -50%)`; this._frame = requestAnimationFrame(this._tick); }; onMount(): void { window.addEventListener("pointermove", this._handlePointerMove); this._frame = requestAnimationFrame(this._tick); } onUnmount(): void { window.removeEventListener("pointermove", this._handlePointerMove); cancelAnimationFrame(this._frame); } render() { return ( <div ref={this.elRef} data-slot="smooth-cursor" class={cn("pointer-events-none fixed top-0 left-0 z-100", this.class)} style={{ width: `${String(this.size)}px`, height: `${String(this.size)}px` }} > {this.children ?? <span class="block size-full rounded-full bg-foreground mix-blend-difference" />} </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 } 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 SmoothCursorStyles extends Stylesheet { $root = this.css({ pointerEvents: "none", position: "fixed", top: 0, left: 0, zIndex: 100 }); $dot = this.css({ display: "block", height: "100%", width: "100%", borderRadius: "9999px", backgroundColor: t.foreground, mixBlendMode: "difference" });}export interface SmoothCursorProps { size?: number; ease?: number; class?: string; children?: Children;}/** Writes `transform` directly rather than through `@State`, since the rAF lerp loop needs to keep ticking every frame regardless of pointer movement. */@Component()export class SmoothCursor extends StatefulComponent { @Styled(SmoothCursorStyles) $s!: SmoothCursorStyles; @Prop() size = 24; @Prop() ease = 0.15; @Prop() class?: string; @Prop() children?: Children; @Ref<HTMLDivElement>() elRef!: RefType<HTMLDivElement>; private _target = { x: 0, y: 0 }; private readonly _current = { x: 0, y: 0 }; private _frame = 0; private readonly _handlePointerMove = (event: PointerEvent) => { this._target = { x: event.clientX, y: event.clientY }; }; private readonly _tick = () => { this._current.x += (this._target.x - this._current.x) * this.ease; this._current.y += (this._target.y - this._current.y) * this.ease; const el = this.elRef.current; if (el) el.style.transform = `translate(${String(this._current.x)}px, ${String(this._current.y)}px) translate(-50%, -50%)`; this._frame = requestAnimationFrame(this._tick); }; onMount(): void { window.addEventListener("pointermove", this._handlePointerMove); this._frame = requestAnimationFrame(this._tick); } onUnmount(): void { window.removeEventListener("pointermove", this._handlePointerMove); cancelAnimationFrame(this._frame); } render() { return ( <div ref={this.elRef} data-slot="smooth-cursor" class={cx(this.$s.$root, this.class)} style={{ width: `${String(this.size)}px`, height: `${String(this.size)}px` }} > {this.children ?? <span class={this.$s.$dot} />} </div> ); }}Examples
About
Purely presentational — no Morphos equivalent. Distinct from Pointer (bounded to a single
container, tracks 1:1): mount this once near the app root. Runs its own
requestAnimationFrame loop and writes transform directly rather than through @State, since
the lerp needs to keep ticking every frame regardless of whether the pointer is moving. Pair with
a global cursor: none where you want the native cursor hidden.
Usage
import { SmoothCursor } from "@/components/ui/smooth-cursor";
// once, near the app root
<SmoothCursor size={24} ease={0.15} />Props
| Prop | Type | Default |
|---|---|---|
size | number (px) | 24 |
ease | number (0-1 lerp factor per frame) | 0.15 |
class | string | — |