humanette

Get started

Add natural cursor movement and visible clicks to your Playwright scripts. Use the locators you already know. All cursor assets are bundled.

npm install humanette playwright
npx playwright install chromium

Run your first script

Save this as demo.ts, replace the URL and locators with your app’s, then run bun demo.ts. This opens a visible browser and performs real input.

import { chromium } from 'playwright';
import { createHuman } from 'humanette';
const browser = await chromium.launch({ headless: false });
try {
const page = await browser.newPage();
await page.goto('http://localhost:3000'); // Your app
const human = await createHuman(page, { scale: 4 });
try {
await human.click(page.getByRole('button', { name: 'Save' }));
await human.type(page.getByLabel('Title'), 'Start', { selectAll: true });
await human.selectText(page.getByText('Select these words.'));
await human.drag(page.getByTestId('card'), page.getByTestId('drop'));
} finally {
await human.dispose();
}
} finally {
await browser.close();
}

Await actions in order. Keep normal Playwright navigation, assertions, and app-readiness checks; use Humanette for the actions you want people to follow.

API reference

Prefer page.getByRole(), page.getByLabel(), or page.getByTestId(). CSS selectors and manual coordinates work too. Durations are in milliseconds.

await human.click(page.getByRole('button', { name: 'Save' }));
await human.moveTo({ x: 320, y: 180 });
await human.drag({ x: 100, y: 200 }, { x: 600, y: 200 });

createHuman(page, options?)

Attach to your existing Playwright page. Options include scale (cursor size), seed (repeatable paths), settle (aim pause, 120 ms by default), and fps (input sampling target, not recording FPS).

human.click(target, { duration?, signal? })

Move to the target, slow down on approach, pause to aim, then send real mouse down/up.

human.moveTo(target, { duration?, signal? })

Move without clicking. A locator resolves to its center after scrolling into view. Coordinates are CSS pixels relative to the main viewport.

human.selectText(locator, { duration?, signal? })

Drag from the first visible text line to the last. Accepts a locator or selector for ordinary selectable text, including nested spans and wrapped lines. For inputs, use type with selectAll or a keyboard shortcut.

human.drag(from, to, { duration?, signal? })

Move to the source, hold the mouse, drag to the destination, then release. Sources and destinations may be locators, selectors, or coordinates. The mouse is released even when the drag is cancelled.

human.type(target, text, { delay?, selectAll?, signal? })

Click the target and insert text through Playwright keyboard input. Set selectAll to replace existing contents. Delay is per character in milliseconds.

human.press(key)

Send a Playwright keyboard shortcut, for example ControlOrMeta+A or Shift+2.

human.wait(milliseconds, signal?)

Add a short presentation pause. Use Playwright assertions or locator waits for application readiness.

human.configure(appearance)

Change cursor size and feedback while the script is running. For example, await human.configure({ scale: 4, textSelectionOpacity: 0.4 }).

human.dispose()

Remove the cursor overlay and its listeners. Await any running action before cleanup. Your Playwright page stays open.

Appearance options include scale, color, filled, motionBlur, textSelectionOpacity, pressRadius, pressDuration, pressOpacity, pressScale, holdRadius, holdOpacity, ringWidth, releaseRadius, and releaseDuration. Tune them in Pointer Lab.

Record with Playwright

Playwright records the page, including Humanette’s cursor. This example saves to one fixed filename. Recording starts when the page is created, so navigation and loading are included; trim that lead-in for a finished product video.

import { chromium } from 'playwright';
import { createHuman } from 'humanette';
const browser = await chromium.launch();
try {
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
deviceScaleFactor: 2,
recordVideo: {
dir: 'out',
size: { width: 1280, height: 800 },
},
});
const page = await context.newPage();
const video = page.video()!;
try {
await page.goto('http://localhost:3000');
await page.getByRole('button', { name: 'Save' }).waitFor();
const human = await createHuman(page, { scale: 4, seed: 42 });
try {
await human.click(page.getByRole('button', { name: 'Save' }));
await human.wait(600); // Brief hold on the result
} finally {
await human.dispose();
}
} finally {
await context.close(); // Finalize the video before saving
}
await video.saveAs('out/demo.webm');
await video.delete(); // Remove Playwright’s generated filename
} finally {
await browser.close();
}

The video above is explicitly 1280 × 800. A device scale factor of 2 does not make that video Retina resolution. Rehearse your script, avoid long pauses, and inspect the actual output before increasing resolution or frame rate.

In this repository, run bun dev, then bun example:playwright. To watch without recording, use bun example:playwright:headful. Compare live movement with the recording before attributing stutter to input.

Limitations

Humanette is not a recorder and does not guarantee capture FPS. Input delivery and distinct captured frames depend on browser workload and the recorder. A high-FPS file can still contain repeated frames.

Locators are measured before movement; targets that move during the approach can require another attempt. The adapter does not reproduce all of Playwright locator.click’s actionability checks. Text selection assumes ordinary left-to-right selectable content; use coordinates or keyboard commands for specialized editors.

CSS cursor inference is limited across closed shadow roots, cross-origin frames, native controls, and custom cursor images. Busy cursors are static. No telemetry, recording uploads, or remote asset requests.