Animated Cursor

import { AnimatedCursor } from '@hanzo/ui'

Default

One cursor for the whole page: a dot with an eight-point trail that grows over a button, turns into a bar over a text field and squares over anything marked `data-cursor="grab"`; the first button puts the native cursor back.

Drag me

export function Default() {
const [on, setOn] = useState(false)
return (
<YStack gap="$3" items="flex-start">
<AnimatedCursor isVisible={on} hideOnTouch={false} hoverScale={2} />
<Button variant={on ? "primary" : "outline"} onPress={() => setOn(!on)}>
{on ? "Native cursor" : "Animated cursor"}
</Button>
<XStack gap="$3" items="center" flexWrap="wrap">
<Button variant="outline">A button</Button>
<Input type="text" placeholder="A text field" />
<Paragraph data-cursor="grab" color="$quiet">
Drag me
</Paragraph>
</XStack>
</YStack>
)
}

Blend mode

`difference` inverts whatever the dot crosses; a larger dot, no trail, and a slower settle.

Move over this inverted panel

export function BlendMode() {
const [on, setOn] = useState(false)
return (
<YStack gap="$3" items="flex-start">
<AnimatedCursor isVisible={on} hideOnTouch={false} blendMode="difference" color="#ffffff" showTrail={false} size={32} animationDuration={400} />
<Button variant={on ? "primary" : "outline"} onPress={() => setOn(!on)}>
{on ? "Native cursor" : "Animated cursor"}
</Button>
<YStack p="$5" rounded="$4" bg="$ink">
<Paragraph color="$sunken">Move over this inverted panel</Paragraph>
</YStack>
</YStack>
)
}

Types

export type AnimatedCursorBlendMode =
| 'normal'
| 'multiply'
| 'screen'
| 'overlay'
| 'darken'
| 'lighten'
| 'color-dodge'
| 'color-burn'
| 'hard-light'
| 'soft-light'
| 'difference'
| 'exclusion'
export type AnimatedCursorShape = 'default' | 'pointer' | 'text' | 'grab' | 'grabbing'
export type CursorPosition = { x: number; y: number }
export type CursorState = {
position: CursorPosition
isHovering: boolean
isClicking: boolean
cursorType: AnimatedCursorShape
}
export type AnimatedCursorProps = {
/** Whether the cursor renders at all; off, the native cursor is untouched. */
isVisible?: boolean
/** Diameter of the dot, in px. */
size?: number
/** Fill of the dot: a theme token such as `$color`, or a CSS colour. */
color?: string
/** Fill of each trailing dot; by default the fill at a third of its strength. */
trailColor?: string
/** How long the dot and the ring take to settle, in ms. */
animationDuration?: number
/** Whether a fading trail of past positions renders behind the dot. */
showTrail?: boolean
/** How many past positions the trail keeps. */
trailLength?: number
/** `mix-blend-mode` for the dot, the trail and the hover ring. */
blendMode?: AnimatedCursorBlendMode
/** Scale the dot reaches while hovering an interactive element. */
hoverScale?: number
/** Render nothing on a device that reports touch support. */
hideOnTouch?: boolean
/** Class notation for the fixed frame. */
className?: string
/** How long the click scale takes to release, in ms. */
clickAnimationDuration?: number
/** Stacking context of the frame, and of the dot within it; the trail and ring sit just under. */
zIndex?: number
}

Exports

AnimatedCursortype AnimatedCursorBlendModetype AnimatedCursorPropstype AnimatedCursorShapetype AnimatedCursorPositiontype CursorState