---
title: The agent cursor
description: How the ghost cursor moves, waits for a confirmation and gets out of the person's way, plus its options, the spotlight and how to restyle it.
related:
  - /concepts/how-it-works
  - /build/confirm-before-it-runs
  - /reference/core/install-agent-visuals
---

The cursor is the visible half of WebMCP Agents. It shows _where_ the agent is working and _what_ it is typing, parks
where the person has to decide, and disappears the moment they move.

The provider installs it before its first tool registers (the [installation](/docs/installation) pages show the line), so the
rest of this page is what the person sees and what you can tune.

## Yielding to the person

<video autoPlay loop muted playsInline data-no-zoom width="960">
  <source src="/blume-assets/content/_/8dd9c9593dc42e04.webm" type="video/webm" />
  <source src="/blume-assets/content/_/9e98d0b2496484ed.mp4" type="video/mp4" />
</video>

*The person scrolls while the agent is adding to the cart: the cursor vanishes and the tool still completes.*

A real person always wins. When they click, scroll, touch or press a key, the
cursor, its tag and the spotlight fade out in 80 ms, and every pending step fast-forwards: moves arrive at once, typing
writes the final value, and a held press or `execute()` runs immediately. **The tool still completes and answers the
agent**; it just stops waiting for the animation.

- **Only trusted input counts.** The agent's own fills never make the cursor yield.
- **Moving the pointer is not using the page.** Someone who starts a call and moves the mouse away still sees it
  run. Pass `yieldToUser: { moveThreshold: 6 }` for a cursor that hides the moment the pointer moves.
- **A confirmation keeps its ring.** While the browser waits for a confirming click, the ring on that button stays, so
  the person knows where to confirm. Once they go quiet the cursor returns to it.
- **A press still leaves a mark.** A control pressed while the cursor was out of the way gets the ring for a moment,
  so a call made while the person was busy is not a silent one.
- **A background tab counts as away.** No frame is drawn there, so calls run at once instead of waiting for a cursor
  nobody sees.
- **It comes back on the next call.** After `resumeAfter` ms (800 by default) without input, the next tool call shows
  the cursor again, entering from where the person's pointer was.
- **Your own agent UI never counts.** Input inside an element marked with `data-web-agents-ui`
  ([`UI_HOST_ATTR`](/docs/reference/core/ui-host-attr)) is the person driving the agent from a chat or the devtools. The chat
  and the devtools set it; mark your own panel the same way.

## Confirmation

<video autoPlay loop muted playsInline data-no-zoom width="960">
  <source src="/blume-assets/content/_/004d6e5835f581b7.webm" type="video/webm" />
  <source src="/blume-assets/content/_/3fa00c17f4341102.mp4" type="video/mp4" />
</video>

*The agent fills checkout and the cursor parks on Place order until the person clicks it.*

A tool with **`requireConfirmation: true`** makes the agent wait for the person: the cursor types what it can, then
parks on the tool's control with a `Click "…" to confirm` prompt, and the call waits for a trusted click on it. That
click is the only thing that completes the call. It waits 10 seconds: after that the agent is told the person did
not confirm (`needs_user`), and the prompt goes. A click elsewhere or a scroll ends it at once (`cancelled`). A
[declarative form](/docs/more/declarative-forms) without `toolautosubmit` gets the same pause from the browser.

The control that is parked on carries `data-agent-active`, and the cursor has the `is-waiting` class (what the example
apps' tests look for before they click). If the name of the control matches two buttons, or none, the call fails with
`unavailable` instead of parking somewhere wrong. [Confirm before it runs](/docs/build/confirm-before-it-runs) is the page
for writing one.

A waiting call always shows where to confirm:

- **Out of view.** The control is scrolled to once. If the person then scrolls away, the page is left alone and the
  cursor waits at the edge of the window nearest the control, prompt beside it.
- **Busy when the call started.** Nothing scrolls under a person who is using the page. When the person goes quiet, the page scrolls
  to the control and the cursor parks on it.
- **The control changes.** If the tool's `target` now finds another control (a header link that folds into a menu on
  scroll, leaving the footer's), the cursor and `data-agent-active` move to it, and a click there confirms.
- **The agent calls something else meanwhile.** The cursor does that call's work without the prompt, then returns to
  the waiting control. An agent cannot dismiss a confirmation it asked for.
- **The person runs something else.** A call the person starts themself (a button or chip that runs a tool, the
  devtools) cancels the waiting one: the agent reads `cancelled`, and the prompt goes.
- **The control is hidden.** With no control left to click (removed, disabled, or hidden by CSS), the call fails with
  `unavailable` instead of waiting for ever.

## How it moves

<video autoPlay loop muted playsInline data-no-zoom width="960">
  <source src="/blume-assets/content/_/c2cca8e38df5b44e.webm" type="video/webm" />
  <source src="/blume-assets/content/_/f313c0f93dee2b17.mp4" type="video/mp4" />
</video>

*Minimum-jerk travel, a spring on arrival, the values typed in place.*

The cursor goes to the tool's control, then to each field it types into, then back to press the control. One
`requestAnimationFrame` loop drives everything, and every animation uses transforms and opacity only.

| Part | What it does |
|---|---|
| **Curved paths** | The target point travels an arc with a minimum-jerk profile, the curve a human reach follows. The arrow chases it on a near-critically-damped spring, so a new move mid-flight bends the path instead of restarting it. |
| **Fitts's-law timing** | Travel takes 180 to 650 ms depending on distance and target size, and each step waits for _arrival_, not a fixed delay. |
| **Anchored until the click** | Positions are re-read every frame, so the cursor rides along with scrolling and layout shifts. |
| **A press in sight** | A control outside the window is scrolled to the middle first, and the cursor waits inside the window, at the edge nearest it, until it is there: the press never happens off screen. A control in a sticky or fixed box (a header, a drawer) that is already in view is left where it is. |
| **Gone when the control is** | After the click the cursor rests on the control for `linger` ms. If the control is no longer under it (a drawer opened over it, the list shifted, it was removed), the cursor fades out at once instead of pointing at the wrong thing. |
| **At rest** | It sways gently around its tip, a little more while waiting for the person. |
| **Reduced motion** | Under `prefers-reduced-motion`, everything snaps and fades. |

Tool status is also announced in a polite live region for screen readers.

## Spotlight

`spotlight: true` blurs and dims the page except the control being worked in (for a declarative form, the whole
`[toolname]` form), and the clear window glides from tool to tool. Tune it with
`spotlight: { blur: 4, dim: 0.08, padding: 12 }`. It never takes pointer events, so the person can still click to
confirm.

## Options

You pass these as the provider's `visuals` prop, or to `installAgentVisuals()` yourself.

| Option | Default | What it does |
|---|---|---|
| `sync` | `true` | Hold the agent's call until the cursor has typed and reached the control, so the page reacts at the click ([how](/docs/concepts/how-it-works#the-page-reacts-at-the-click)). |
| `spotlight` | `false` | Blur and dim everything except the tool being worked in. `true`, or `{ blur, dim, padding }`. |
| `yieldToUser` | `true` | Hide the cursor when the person interacts. `false`, or `{ moveThreshold, resumeAfter, ignore }`. |
| `linger` | `1200` | Milliseconds the cursor stays after a tool finishes. |
| `name` | `""` | A tag shown beside the cursor. |
| `log` | none | Receives every [agent event](/docs/reference/core/on-agent-event). |

`installAgentVisuals()` returns `{ setSpotlight, setYieldToUser, destroy }`, so both switches can change at runtime (the
devtools' Settings tab is wired to them). Every option and the return value:
[`installAgentVisuals`](/docs/reference/core/install-agent-visuals).

## Styling

The cursor is a black multiplayer-style arrow with a white edge, readable on light and dark surfaces. Its colours are
custom properties at the top of `@webmcp-agents/core/cursor.css`. To brand it, override them in a stylesheet that loads
after it:

```css styles.css
:root {
  --agent: #4f46e5;        /* fill for the arrow and the tag */
  --agent-strong: #4338ca; /* outlines, rings and borders */
  --agent-ink: #ffffff;    /* text on the tag */
  --agent-wait: #4f46e5;   /* while waiting for the person to confirm */
}
```

[`cursor.css`](/docs/reference/core/cursor-css) lists every custom property, class and state.

:::warning[Don't use showModal() for drawers]
The top layer of a modal `<dialog>` hides the cursor (and makes the devtools inert). Use a non-modal `role="dialog"` panel
and make the page behind it `inert` instead, as the [examples](/docs/examples/field-store) do.
:::
