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.
| Category | Cursors |
|---|---|
| Type | Web Component (<mv-cursor>) |
| Status | stable |
| Keywords | cursor, pointer, follower, spring, blend, difference, label, smooth-cursor |
When to use
- A portfolio or agency site wants a custom dot-and-ring cursor that grows over links
- Cards or media should show a cursor label such as 'View' or 'Drag' on hover
- A single area, rather than the whole page, needs a custom or difference-blend cursor
Avoid when
- Productivity apps, dashboards or dense forms where users expect the native cursor
Install
node scripts/add.mjs cursor-follower --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
scope | CSS selector | Area where the cursor is replaced (default: the element's own box). | |
global | boolean | Whole page (place a single empty <mv-cursor global> once). | |
variant | blend | Solid disc with mix-blend-mode: difference (inverts what it hovers). | |
targets | CSS selector | a[href], button, [role=button], summary, label, [data-cursor] | Elements that make the ring grow. |
disabled | boolean | Restores the native cursor. | |
data-cursor (on your elements) | text | (empty) | native | Text = solid ring with a label; empty = hover state; native = keep the system cursor. |
Content structure
| Name | Description |
|---|---|
(content) | Content of the area when the element itself is the area. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-cursor-size | 34px (blend: 28px) | Ring diameter. |
--mv-cursor-dot-size | 6px | Dot diameter. |
--mv-cursor-color | var(--mv-fg) | Dot and ring color. |
--mv-cursor-accent | var(--mv-accent) | Hover and label color. |
--mv-cursor-blend-color | white | Disc 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.