Cursor Follower — <mv-cursor>

Custom cursor: a precise dot and a ring that follows on a spring, stretching with speed, grows over interactive elements and shows a label (data-cursor="View"), with a difference-blend variant; scoped to an area or the whole page.

CategoryCursors
TypeWeb Component (<mv-cursor>)
Statusstable
Keywordscursor, pointer, follower, spring, blend, difference, label, smooth-cursor

When to use

Avoid when

Install

node scripts/add.mjs cursor-follower --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["cursor-follower"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, components/cursor-follower/cursor-follower.js, components/cursor-follower/cursor-follower.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;width:100%">
  <mv-cursor id="mv-cursor-demo" style="border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);padding:1.25rem;min-height:300px">
    <div style="display:grid;gap:.9rem">
      <div style="display:flex;justify-content:space-between;align-items:center;font-size:.85rem">
        <strong>North Studio</strong>
        <nav style="display:flex;gap:.9rem"><a href="#" style="color:var(--mv-fg-muted);text-decoration:none">Work</a><a href="#" style="color:var(--mv-fg-muted);text-decoration:none">Studio</a></nav>
      </div>
      <a href="#" data-cursor="View" style="display:block;aspect-ratio:16/7;border-radius:var(--mv-radius-lg);background:linear-gradient(120deg,oklch(0.72 0.12 200),oklch(0.55 0.16 260) 60%,oklch(0.4 0.14 300));position:relative;overflow:hidden" aria-label="Glacier project, brand identity">
        <span style="position:absolute;left:.9rem;bottom:.7rem;color:white;font:600 .95rem/1.2 var(--mv-font-sans)">Glacier — brand identity</span>
      </a>
      <div style="display:flex;gap:.5rem;align-items:center">
        <input class="mv-input" placeholder="Your email (native cursor here)" style="flex:1;min-width:0">
        <button class="mv-button">Subscribe</button>
      </div>
      <small style="color:var(--mv-fg-subtle)">Hover around: the ring follows on a spring, grows over links and shows “View” on the image.</small>
    </div>
  </mv-cursor>

  <mv-cursor variant="blend" style="display:grid;place-items:center;border-radius:var(--mv-radius-xl);background:var(--mv-fg);color:var(--mv-bg);padding:1.25rem;min-height:300px;text-align:center">
    <div style="display:grid;gap:.75rem;justify-items:center">
      <p style="margin:0;font:700 clamp(1.6rem,3.2vw,2.4rem)/1.05 var(--mv-font-sans);letter-spacing:-.03em">We design<br>brands that feel alive.</p>
      <a href="#" style="color:inherit;font-size:.9rem">Read the manifesto →</a>
      <span data-cursor="Hello" style="font-size:.8rem;opacity:.7;padding:.4rem .8rem;border:1px solid currentColor;border-radius:999px">Hover here</span>
    </div>
  </mv-cursor>
</div>

API

Attributes

NameTypeDefaultDescription
scopeCSS selectorArea where the cursor is replaced (default: the element's own box).
globalbooleanWhole page (place a single empty <mv-cursor global> once).
variantblendSolid disc with mix-blend-mode: difference (inverts what it hovers).
targetsCSS selectora[href], button, [role=button], summary, label, [data-cursor]Elements that make the ring grow.
disabledbooleanRestores the native cursor.
data-cursor (on your elements)text | (empty) | nativeText = solid ring with a label; empty = hover state; native = keep the system cursor.

Content structure

NameDescription
(content)Content of the area when the element itself is the area.

CSS variables

NameDefaultDescription
--mv-cursor-size34px (blend: 28px)Ring diameter.
--mv-cursor-dot-size6pxDot diameter.
--mv-cursor-colorvar(--mv-fg)Dot and ring color.
--mv-cursor-accentvar(--mv-accent)Hover and label color.
--mv-cursor-blend-colorwhiteDisc color of the blend variant.

Accessibility

Purely visual (aria-hidden layer, pointer-events: none, attached to <body>). Only active with (hover: hover) and (pointer: fine) and without reduced motion: otherwise, and on touch, the native cursor stays. The native cursor is never hidden over inputs, textareas, selects, editable content or [data-cursor="native"] (the follower fades out there). Everything is restored when the element is removed; the spring loop stops as soon as the cursor is still. data-cursor labels are visual only: the element must keep its own accessible name.