Cursor

A custom cursor, and a label that follows it, inside an area or across the page.

<script setup lang="ts">
import type { CursorFollowAlign, CursorFollowSide } from '@animate-ui/vue/primitives/cursor'
import { Cursor, CursorContainer, CursorFollow, CursorProvider } from '@animate-ui/vue/primitives/cursor'

const props = withDefaults(
  defineProps<{
    global?: boolean | string
    enableCursor?: boolean | string
    enableCursorFollow?: boolean | string
    side?: CursorFollowSide
    sideOffset?: number | string
    align?: CursorFollowAlign
    alignOffset?: number | string
  }>(),
  {
    global: false,
    enableCursor: true,
    enableCursorFollow: true,
    side: 'bottom',
    sideOffset: 15,
    align: 'end',
    alignOffset: 5,
  },
)

const on = (value: boolean | string) => value === true || value === 'true'
</script>

<template>
  <div class="flex h-72 w-full max-w-md items-center justify-center rounded-xl bg-accent">
    <p class="font-medium text-muted-foreground italic">
      Move your pointer over this area
    </p>
    <CursorProvider :global="on(props.global)">
      <CursorContainer>
        <Cursor v-if="on(enableCursor)">
          <svg
            class="size-6 text-foreground"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 40 40"
            aria-hidden="true"
          >
            <path
              fill="currentColor"
              d="M1.8 4.4 7 36.2c.3 1.8 2.6 2.3 3.6.8l3.9-5.7c1.7-2.5 4.5-4.1 7.5-4.3l6.9-.5c1.8-.1 2.5-2.4 1.1-3.5L5 2.5c-1.4-1.1-3.5 0-3.3 1.9Z"
            />
          </svg>
        </Cursor>
        <CursorFollow
          v-if="on(enableCursorFollow)"
          :side="side"
          :side-offset="Number(sideOffset)"
          :align="align"
          :align-offset="Number(alignOffset)"
        >
          <div class="rounded-md bg-foreground px-2 py-1 text-sm text-background">
            Designer
          </div>
        </CursorFollow>
      </CursorContainer>
    </CursorProvider>
  </div>
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import {
  Cursor,
  CursorContainer,
  CursorFollow,
  CursorProvider,
} from '@animate-ui/vue/primitives/cursor';
</script>

<template>
  <div class="relative h-72">
    <CursorProvider>
      <CursorContainer>
        <Cursor>
          <ArrowIcon />
        </Cursor>
        <CursorFollow side="bottom" align="end">
          <span>Designer</span>
        </CursorFollow>
      </CursorContainer>
    </CursorProvider>
  </div>
</template>

By default, the cursor lives in the parent of CursorContainer. With global, it replaces the pointer on the whole page.

Animation

  • Entering the area hides the native pointer and pops the cursor and the follower in: they scale up from 0 on a spring with a little overshoot (stiffness 550, damping 30) while fading in (300ms). Leaving scales them back to 0 without overshoot and fades them out, then restores the pointer.
  • Cursor sticks to the pointer, its top-left corner on it (where an arrow's tip goes). CursorFollow trails it on a spring without bounce (stiffness 500, damping 50), so it feels attached without being rigid. It springs from where it last was: the first time, it flies in from near the area's top-left corner.
  • The position is driven by motion values with a hardware-accelerated transform (x / y): moving the pointer never re-renders Vue.
  • Touch pointers are ignored: a custom cursor only makes sense for pointers that hover.
  • <AnimateUIProvider :tempo> paces the pop and the follow spring.
  • Under reduced motion, the cursors only fade in and out, and CursorFollow sticks to the pointer too.

API Reference

CursorProvider

PropTypeDefault
global?
booleanfalse
#default
slot-

CursorContainer

Marks the area: the cursor shows inside its parent element.

PropTypeDefault
as-child?
booleanfalse
...props?
HTMLMotionProps<'div'>-

Cursor

Its top-left corner sits on the pointer.

PropTypeDefault
as-child?
booleanfalse
#default
slot-
...props?
HTMLMotionProps<'div'>-

CursorFollow

PropTypeDefault
as-child?
booleanfalse
side?
'top' | 'right' | 'bottom' | 'left''bottom'
side-offset?
number0
align?
'start' | 'center' | 'end''end'
align-offset?
number0
transition?
SpringOptions{ stiffness: 500, damping: 50, bounce: 0 }
#default
slot-
...props?
Omit<HTMLMotionProps<'div'>, 'transition'>-

useCursor

Returns the cursor state: cursorX and cursorY motion values, the active ref, a global() getter, and the container and cursor element refs.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026