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.
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 or anywhere you need “the button called Remove Ocean Tee”.
Import
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.
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. 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
roleattribute wins. Otherwise a role comes from the element:<button>and<summary>arebutton,<a href>islink,<select>iscombobox,<textarea>istextbox, and an<input>is its type’s role (submit,reset,imageandbuttonarebutton;checkbox;radio;rangeisslider;numberisspinbutton;searchissearchbox;hiddenhas none; any other type istextbox). Onlybutton,a[href],input,select,textarea,summaryand elements with aroleattribute 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 itsvalue. 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
:disabledoraria-disabled="true", not inside aninertoraria-hidden="true"part of the page, and not inside library UI (an element markeddata-web-agents-ui, 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 functiontargetthat returns the translated name. - Finding a field. Without a
fieldsentry, a parameter’s field is searched from the tool’s control outward, one ancestor at a time, so the closest wins: a control whosenameattribute is the parameter (for radios, the one whosevalueis the parameter’s value), else one labelled with the schema’stitlefor the parameter, else one labelled with the parameter read as words (firstNameis “first Name”, compared without case). With no control to start from, nothing is inferred: name the field infields. - 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, where the cursor goes
defineWebToolinspectToolsUI_HOST_ATTR