A soft dot or ring that trails the real pointer with easing, inside a container or across the page. The system cursor stays unless you ask to hide it.
Optional, and kept apart.
Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.
import { SmoothCursor } from "@rdloom/react";
export default function SmoothCursorBasicExample() {
return (
<div className="flex w-full justify-center">
<SmoothCursor className="flex h-48 w-full max-w-md items-center justify-center rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-subtle)]">
<p className="text-sm text-[var(--rd-color-text-muted)]">Move the mouse over this area.</p>
</SmoothCursor>
</div>
);
}Installation
npx rdloom add smooth-cursorCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.
It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/smooth-cursor.jsonWorks, but without upgrade tracking, and you import the motion CSS yourself.
Usage
import { SmoothCursor } from "@rdloom/react";
<div className="flex w-full justify-center">
<SmoothCursor className="flex h-48 w-full max-w-md items-center justify-center rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-subtle)]">
<p className="text-sm text-[var(--rd-color-text-muted)]">Move the mouse over this area.</p>
</SmoothCursor>
</div>Ring in a card
import { SmoothCursor } from "@rdloom/react";
export default function SmoothCursorRingExample() {
return (
<div className="flex w-full justify-center">
<SmoothCursor
shape="ring"
size={36}
smoothing={0.08}
className="w-full max-w-md rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] p-6"
>
<h3 className="text-base font-medium">Studio plan</h3>
<p className="mt-1 text-sm text-[var(--rd-color-text-muted)]">A slow ring trails the pointer. Your own pointer stays visible.</p>
<button
type="button"
className="mt-4 rounded-[var(--rd-radius-control)] bg-[var(--rd-color-action-primary)] px-3 py-1.5 text-sm text-[var(--rd-color-action-on-primary)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]"
>
Choose plan
</button>
</SmoothCursor>
</div>
);
}Page wide
With scope set to page, a small dot trails the pointer everywhere in the window. Touch and reduced motion never see it.
import { SmoothCursor } from "@rdloom/react";
export default function SmoothCursorPageWideExample() {
return (
<div className="flex w-full justify-center">
<SmoothCursor scope="page" size={14} color="var(--rd-color-action-primary)" />
<p className="max-w-sm text-center text-sm text-[var(--rd-color-text-muted)]">
With scope set to page, a small dot trails the pointer everywhere in the window. Touch and reduced motion never see it.
</p>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
childrenContent of the container. With scope "page" the children are rendered as they are and the follower covers the whole window. | node | none |
scopeWhere the follower lives: inside the wrapper around the children, or over the whole window. | "container" | "page" | "container" |
shapeA filled dot or an open ring. | "dot" | "ring" | "dot" |
sizeDiameter in pixels. | number | 20 |
colorAny CSS colour. Defaults to the primary action colour. | string | none |
smoothingHow quickly the follower catches up, from 0.02 (lazy) to 1 (no lag). | number | 0.15 |
hideNativeCursorHides the system pointer over the container once a mouse is in use. Off by default: keep it off unless the follower is clearly visible everywhere. | boolean | false |
isStillSwitches the follower off and leaves the system pointer alone. | boolean | false |
Accessibility
Role presentation, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
Not interactive.
Screen readers announce
- Not announced: the follower is purely visual
What your code must do
- Decoration for mouse users: the follower is aria-hidden, ignores the pointer (pointer-events none) and never takes focus
- It never replaces keyboard focus: focus rings are untouched and nothing depends on where the follower is
- Not shown for touch or pen input, for prefers-reduced-motion, with isStill, or inside rdm-still
- The system pointer is only hidden when hideNativeCursor is set, and only after a mouse has moved over the container
- The frame loop runs only while the follower is catching up and stops when it arrives; listeners are removed on unmount
- Forced-colors: the follower keeps its own colour (forced-color-adjust none) so it stays visible
Guidelines
Use it when
- A playful hero or showcase area for mouse users
- A soft highlight that follows the pointer over a canvas or gallery
Avoid it when
- Forms, dashboards and anywhere precise pointing matters
- Anything that would be unusable without the follower
Don't
- Hiding the system pointer over text fields and small controls
- Using it as the only hover feedback
- Very large or fully opaque followers that cover what is being pointed at
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-action-primary