---
description: "Finds a control on the page the way a person, or a Testing Library query, does: by its role and the name on it."
---

# `locate`

Finds a control on the page the way a person, or a Testing Library query, does: by its role and the name on it. It is how a tool's cursor knows where to go without anything added to the markup, and you can use it yourself, in a function [`target`](/docs/reference/core/define-web-tool#where-the-cursor-goes) or anywhere you need "the button called Remove Ocean Tee".

## Import

```ts
import { locate } from "@webmcp-agents/core";
import type { FieldTarget, ToolLocator } from "@webmcp-agents/core";
```

## Example

Three lookups, and a tool whose `target` picks one button per call.

```ts title="locate-controls.ts"
import { defineWebTool, locate } from "@webmcp-agents/core";
import { z } from "zod";

const names: Record<string, string> = { a1: "Ocean Tee" };

// A button by its role and name.
export const remove = locate({
  role: "button",
  name: "Remove Ocean Tee",
});

// A link whose name is a pattern, anywhere on the page.
export const cart = locate({ name: /^Cart, \d+ items?$/ });

// A field, only inside the open drawer.
export const quantity = locate({
  role: "spinbutton",
  name: "Quantity",
  within: "#cart-drawer",
});

// As a tool's `target`: one control per call.
export const removeFromCart = defineWebTool({
  id: "remove_from_cart",
  description: "Remove one line from the cart. Ids: get_cart.",
  inputSchema: z.object({ lineId: z.string() }),
  target: ({ lineId }) => ({
    role: "button",
    name: `Remove ${names[lineId]}`,
  }),
  execute: ({ lineId }) => `Removed ${lineId}.`,
});
```

## Options

`locate(locator)` takes a `ToolLocator`, every part optional. With no part it matches any control.

| Name | Type | Default | Description |
|---|---|---|---|
| `role` | `string` | any role | The ARIA role to look for: `button`, `link`, `tab`, `menuitem`, `switch`, `checkbox`, `radio`, `textbox`, `searchbox`, `spinbutton`, `slider`, `combobox`, `listbox`, or any explicit `role` attribute. Native elements have theirs (see the notes). |
| `name` | `string \| RegExp` | any name | The accessible name. A string compares trimmed, with whitespace collapsed and case ignored; an exact match beats a name that only starts with it. A RegExp is tested against the name as shown. |
| `within` | `string \| Element` | the whole document | Only look inside this element, or inside the first match of this selector. For the same name in two places (a cart drawer and a page). |

### `FieldTarget`

Where one parameter is typed: the values of a tool's [`fields`](/docs/reference/core/define-web-tool#options). `ToolLocator | string | false`:

| Value | Meaning |
|---|---|
| a `ToolLocator` | The control to type into. |
| a `string` | A label's text: the same as `{ name: "…" }`, among the roles a value goes into: `textbox`, `searchbox`, `spinbutton`, `slider`, `combobox`, `listbox`, `checkbox`, `radio`, `switch`. |
| `false` | Never type that parameter (it still reaches `execute`). |

## Returns

The first usable control that matches, in document order, as an `Element`, or `null`. Where several match, exact names win: the controls named exactly `name` when there are any, else the ones whose name starts with it. A tool uses the same rule, and a confirm-gated tool additionally requires that exactly one control matches.

## Notes

- **Roles.** The explicit `role` attribute wins. Otherwise a role comes from the element: `<button>` and `<summary>` are `button`, `<a href>` is `link`, `<select>` is `combobox`, `<textarea>` is `textbox`, and an `<input>` is its type's role (`submit`, `reset`, `image` and `button` are `button`; `checkbox`; `radio`; `range` is `slider`; `number` is `spinbutton`; `search` is `searchbox`; `hidden` has none; any other type is `textbox`). Only `button`, `a[href]`, `input`, `select`, `textarea`, `summary` and elements with a `role` attribute are candidates, which covers the Radix and Base UI widgets shadcn/ui renders.
- **Names**, in order: `aria-labelledby`, `aria-label`, the control's `<label>` (for or wrapping), `placeholder`, its text content (not for `<select>` or `<textarea>`), `alt`, `title`, and for a button its `value`. This is a small purpose-built matcher, not the full accessible-name algorithm: it covers the sources interfaces actually use.
- **Usable.** Only controls a person could use right now count: rendered and visible, not `:disabled` or `aria-disabled="true"`, not inside an `inert` or `aria-hidden="true"` part of the page, and not inside library UI (an element marked [`data-web-agents-ui`](/docs/reference/core/ui-host-attr), such as the devtools or the chat).
- **Icon-only buttons need an `aria-label`**, or they have no name to find. A translated UI uses a function `target` that returns the translated name.
- **Finding a field.** Without a `fields` entry, a parameter's field is searched from the tool's control outward, one ancestor at a time, so the closest wins: a control whose `name` attribute is the parameter (for radios, the one whose `value` is the parameter's value), else one labelled with the schema's `title` for the parameter, else one labelled with the parameter read as words (`firstName` is "first Name", compared without case). With no control to start from, nothing is inferred: name the field in `fields`.
- Nothing touches the DOM at import, and it needs a DOM to run: call it from an event handler, an effect or a tool, not during server rendering.

## See also

- [Define a tool](/docs/build/define-a-tool#how-the-cursor-finds-the-control), where the cursor goes
- [`defineWebTool`](/docs/reference/core/define-web-tool)
- [`inspectTools`](/docs/reference/core/inspect-tools)
- [`UI_HOST_ATTR`](/docs/reference/core/ui-host-attr)
