KQ-1423 · JELLY/CURSOR
Jelly Cursor
Inside its box the pointer becomes a ball of jelly: a soft-body ring of 40 points on damped springs, carried rigidly with the pointer so the cursor stays exact, drawn out into an area-keeping ellipse behind a fast move and set wobbling by a stop, with a damping ratio that `wobble` maps across springs.flick, springs.recoil and looser. Over a button or link it reaches for it side-first and wraps the child's own rounded box as a tint; leaving, the far side clings while the near side follows, until it lets go and snaps back into a ball. Keyboard focus on a child gets the same wrap and Enter or Space squashes it; any key returns the system cursor.
Waylight · reading app
Read at your own pace.
resting · on start reading
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/jelly-cursor
Props
- children
- React.ReactNode
- default —
- The area the jelly lives in. Its buttons and links keep every click, focus and key.
- size
- number
- default 28
- The jelly ball's diameter, in px.
- wobble
- number
- default 0.5
- How long it jiggles once it stops: 0 settles at once (flick), 0.5 bounces twice (recoil), 1 keeps on jiggling.
- color
- "cobalt" | "mint" | "ember" | "ink" | string
- default "cobalt"
- Its colour: a named pigment from the theme tokens, or any CSS colour.
- stretch
- number
- default 0.6
- How far it draws out behind a fast pointer, 0 to 1.
- squash
- number
- default 0.5
- How deep a press squashes it, 0 to 1.
- targets
- string
- default "button, a[href], [role='button'], [data-jelly]"
- Which children it wraps itself round, as a CSS selector.
- padding
- number
- default size × 0.22
- Room it leaves round a wrapped child, in px; a bigger jelly is puffier (6 at the default size).
- stickiness
- number
- default 0.5
- How far it clings to a child you leave before it lets go, 0 to 1.
- tint
- number
- default 0.16
- Its body's opacity while wrapped round a child, so the label reads through, 0 to 1.
- restOn
- string
- default "[data-jelly-rest]"
- The child it rests on with no pointer inside, as a CSS selector; with none it hides.
- hideCursor
- boolean
- default true
- Hide the system cursor while the jelly leads (never over text fields, never after a key press).
- followFocus
- boolean
- default true
- Keyboard focus on a child wraps the jelly round it; Enter or Space squashes it.
- interactive
- boolean
- default true
- The pointer leads the jelly (mouse and pen; a tap moves it to the tapped child).
- onTargetChange
- (target: HTMLElement | null) => void
- default —
- Fires when the child it is wrapped round changes, with that child or null.
- className
- string
- default —
- Classes for the box.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | The area the jelly lives in. Its buttons and links keep every click, focus and key. |
| size | number | 28 | The jelly ball's diameter, in px. |
| wobble | number | 0.5 | How long it jiggles once it stops: 0 settles at once (flick), 0.5 bounces twice (recoil), 1 keeps on jiggling. |
| color | "cobalt" | "mint" | "ember" | "ink" | string | "cobalt" | Its colour: a named pigment from the theme tokens, or any CSS colour. |
| stretch | number | 0.6 | How far it draws out behind a fast pointer, 0 to 1. |
| squash | number | 0.5 | How deep a press squashes it, 0 to 1. |
| targets | string | "button, a[href], [role='button'], [data-jelly]" | Which children it wraps itself round, as a CSS selector. |
| padding | number | size × 0.22 | Room it leaves round a wrapped child, in px; a bigger jelly is puffier (6 at the default size). |
| stickiness | number | 0.5 | How far it clings to a child you leave before it lets go, 0 to 1. |
| tint | number | 0.16 | Its body's opacity while wrapped round a child, so the label reads through, 0 to 1. |
| restOn | string | "[data-jelly-rest]" | The child it rests on with no pointer inside, as a CSS selector; with none it hides. |
| hideCursor | boolean | true | Hide the system cursor while the jelly leads (never over text fields, never after a key press). |
| followFocus | boolean | true | Keyboard focus on a child wraps the jelly round it; Enter or Space squashes it. |
| interactive | boolean | true | The pointer leads the jelly (mouse and pen; a tap moves it to the tapped child). |
| onTargetChange | (target: HTMLElement | null) => void | — | Fires when the child it is wrapped round changes, with that child or null. |
| className | string | — | Classes for the box. |