| name | browserclaw |
| description | Browse websites interactively using the browserclaw library — launch, navigate, snapshot, interact, extract |
Browserclaw Interactive Browsing
You are operating a real browser. You see pages through snapshot() and act on them with click(), type(), select(), etc. Every action may change the page — re-snapshot before deciding what to do next. Refs ("e3", "e8", ...) are only valid for the snapshot that produced them.
Quick start — a full session
import { BrowserClaw } from 'browserclaw';
const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc' });
const page = await browser.currentPage();
try {
const { snapshot, refs } = await page.snapshot({ interactive: true, compact: true });
await page.type('e1', 'Buy milk', { submit: true });
const after = await page.snapshot({ interactive: true, compact: true });
} finally {
await browser.stop();
}
The core loop: Perceive → Reason → Act → Repeat. Never assume an action worked — re-snapshot and confirm.
When not to use browserclaw
If the data is in the initial HTML response and the site doesn't require login, JS rendering, or interactive state, use a plain fetch() / curl. Browserclaw is for pages that need real browser behavior (SPAs, auth flows, forms, JS-rendered content, multi-step interaction).
Snapshot — your eyes
const { snapshot, refs } = await page.snapshot({ interactive: true, compact: true });
Always pass { interactive: true, compact: true } — filters to actionable elements and strips structural noise.
snapshot is a text tree:
- heading "Search Results" [level=2]
- link "Blue Widget - $24.99" [ref=e3]
- button "Next page" [ref=e8]
- combobox "Sort by" [ref=e11]
refs maps ref → element info:
{ "e3": { role: "link", name: "Blue Widget - $24.99" }, ... }
If the snapshot looks empty or skeleton-like (few elements, thin text), the page is still loading. Wait and retry:
await page.waitFor({ loadState: 'networkidle', timeoutMs: 15000 });
for (let i = 0; i < 5; i++) {
const { snapshot } = await page.snapshot({ interactive: true, compact: true });
if (snapshot.split('\n').filter((l) => l.trim()).length > 10) break;
await page.waitFor({ timeMs: 1500 });
}
Core actions
Navigate
await page.goto('https://demo.playwright.dev/todomvc');
Click
await page.click('e8');
Type
await page.type('e2', 'search query');
await page.type('e2', 'search query', { submit: true });
await page.type('e2', 'search query', { slowly: true });
After typing — check for autocomplete before pressing Enter. Re-snapshot and look for combobox, listbox, or suggestion items. If a dropdown appeared, click the right option; pressing Enter usually submits without selecting.
React and other frameworks: prefer type() over fill(). fill() sets the DOM value directly and does not trigger React's onChange. type() simulates keystrokes which do. Use fill() only for batch form filling (below) and for non-framework sites.
Select (dropdowns)
await page.select('e11', 'Price: Low to High');
Press a key
await page.press('Enter');
await page.press('Tab');
await page.press('Escape');
Scroll
await page.scrollIntoView('e15');
await page.evaluate('window.scrollBy(0, 500)');
Screenshot
const buf = await page.screenshot();
Drag / Hover
await page.drag('e3', 'e8');
await page.hover('e2');
Actions without a ref
await page.clickByText('Submit');
await page.clickByText('Save', { exact: true });
await page.clickByRole('button', 'Create', { index: 1 });
await page.clickBySelector('#submit-btn');
await page.mouseClick(400, 300);
evaluate — use it for reading, not acting
const title = await page.evaluate('document.title');
const count = await page.evaluate('document.querySelectorAll(".item").length');
Use evaluate to read DOM state (detection, extraction, queries) that native APIs don't expose. Do not use it to click or type — framework event handlers (React, Angular, Vue) won't fire. For actions, always use page.click(), page.type(), page.press().
Iframes
Elements inside iframes get frame-prefixed refs like f1e23 (frame 1, element 23). The same element may also have a main-page ref, and both appear on the same snapshot line:
- button "Submit Payment" [ref=e63] [ref=f1e82]
For actions inside iframes — especially drag — use the iframe ref (f1e82), not the main-page ref (e63). Using the main-page ref will timeout.
The frame number can change between page loads (f1 one time, f2 the next). Match with regex /f\d+e\d+/, don't hardcode f1.
For JS that reads across frames:
const results = await page.evaluateInAllFrames(`() => {
const el = document.querySelector('input[name="cardnumber"]');
return el ? el.name : null;
}`);
Inspecting page state
await page.url();
await page.title();
await page.pageErrors();
await page.consoleLogs();
await page.networkRequests();
Common patterns
Fill a form
await page.fill([
{ ref: 'e2', value: 'Jane Doe' },
{ ref: 'e4', value: 'jane@acme.test' },
{ ref: 'e6', type: 'checkbox', value: true },
]);
await page.click('e9');
await page.waitFor({ loadState: 'networkidle' });
const { snapshot } = await page.snapshot({ interactive: true, compact: true });
(Remember: for React-controlled inputs that ignore fill(), fall back to type().)
Handle a native dialog
await page.armDialog({ accept: true });
await page.click('e7');
Use page.onDialog(handler) for a persistent handler that handles every dialog until cleared.
Wait for something specific
await page.waitFor({ text: 'Order confirmed' });
await page.waitFor({ selector: '.results-list' });
await page.waitFor({ url: 'checkout/success' });
await page.waitFor({ loadState: 'networkidle' });
await page.waitFor({ timeMs: 2000 });
waitFor({ timeMs }) caps at 30 seconds. For longer waits, loop.
Multi-tab
const tabs = await browser.tabs();
const page2 = await browser.open('https://demo.playwright.dev/svgtodo');
await browser.focus(page.id);
await browser.close(page2.id);
For tabs opened by a click (not by explicit open()), see the tab-manager entry in Agentic skills below.
Extract data from a listing
const items = await page.evaluate(`
Array.from(document.querySelectorAll('.todo-list li')).map(el => ({
text: el.querySelector('label')?.textContent?.trim(),
completed: el.classList.contains('completed'),
}))
`);
Launch options
const browser = await BrowserClaw.launch({
url: 'https://example.com',
headless: false,
ignoreHTTPSErrors: true,
chromeArgs: [
'--disable-web-security',
'--start-maximized',
],
});
To attach to an already-running Chrome: await BrowserClaw.connect('http://localhost:9222').
Recovery
Click didn't work / ref not found — re-snapshot (ref is stale), try a different ref, scrollIntoView(ref) then click, or fall back to clickByText(...) / clickByRole(...).
Repeated action isn't making progress — you may be stuck in a loop. Check page.url() / page.title(), try a different element or navigate away and back.
Form submitted but nothing happened — check the next snapshot for errors, check page.consoleLogs() / page.pageErrors(), then waitFor({ loadState: 'networkidle' }) and re-snapshot.
Agentic skills (browserclaw-agent)
browserclaw ships only the library primitives: snapshots, clicks, types, pressAndHold, etc. Higher-level orchestration — anti-bot solvers, popup dismissal, tab lifecycle, loop detection — lives in a separate project, browserclaw-agent, as a set of composable agentic skills.
Reach for these any time you hit a scenario below. They are the authoritative implementation — don't re-derive them inline. The files aren't importable across the library/agent boundary, so copy the pattern into your own agent rather than trying to import from node_modules.
Key rules
- Never guess the API. When unsure whether a method exists or what it takes, check
node_modules/browserclaw/dist/index.d.ts or node_modules/browserclaw/README.md. Don't invent alternatives.
- Refs are ephemeral. After navigation or a DOM-changing action, re-snapshot.
- Every snapshot uses
{ interactive: true, compact: true }.
- Every extracted value must appear verbatim in a snapshot you actually saw. No fabrication.
- After typing, check for autocomplete before pressing Enter.
- Use native actions (
click, type, press) for interaction; use evaluate only for reading DOM state.