Most Playwright tests need nothing beyond click() and fill(), but hover-triggered dropdowns, Ctrl+click multi-select, and drag implementations that track mousemove events along the path all require the lower-level keyboard and mouse APIs. page.keyboard.type() fires keydown, keypress, keyup, and input events per character, which matters for autocomplete and real-time validation that fill() bypasses. This article covers key presses, modifier key combinations, hover, drag-and-drop with dragTo() and raw mouse events, scroll, and the Ctrl+click multi-select pattern.

Keyboard events

Basic key presses

// Press a single key
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.keyboard.press('Escape');
await page.keyboard.press('ArrowDown');

// Modifier + key combinations
await page.keyboard.press('Control+a');  // Select all
await page.keyboard.press('Control+c');  // Copy
await page.keyboard.press('Control+v');  // Paste
await page.keyboard.press('Shift+Tab'); // Reverse tab
await page.keyboard.press('Meta+k');    // Cmd+K on Mac

Key names follow the KeyboardEvent.key spec. Common ones: 'Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'F1''F12'.

Typing text

// Type into the focused element
await page.getByRole('searchbox').focus();
await page.keyboard.type('playwright testing');

// Type with delay between keystrokes (simulates real typing)
await page.keyboard.type('slow typing', { delay: 50 });

keyboard.type() fires keydown, keypress, keyup, and input events for each character. This matters for applications that handle individual keystrokes (autocomplete, real-time validation, rich text editors).

Hold modifier keys

// Shift + click to select a range
await page.keyboard.down('Shift');
await page.getByRole('row').nth(5).click();
await page.keyboard.up('Shift');

Keyboard navigation testing

Testing that your app is keyboard-accessible is both an accessibility requirement and a QA concern:

test('modal closes on Escape', async ({ page }) => {
  await page.getByRole('button', { name: 'Open modal' }).click();
  await expect(page.getByRole('dialog')).toBeVisible();

  await page.keyboard.press('Escape');

  await expect(page.getByRole('dialog')).not.toBeVisible();
});

test('dropdown navigates with arrow keys', async ({ page }) => {
  await page.getByRole('combobox', { name: 'Country' }).focus();
  await page.keyboard.press('ArrowDown'); // Opens dropdown, selects first
  await page.keyboard.press('ArrowDown'); // Selects second
  await page.keyboard.press('Enter');     // Confirms selection

  await expect(page.getByRole('combobox', { name: 'Country' })).toHaveValue('Australia');
});

Mouse events

Click variants

// Double click
await page.getByRole('row').first().dblclick();

// Right click (context menu)
await page.getByText('Document.pdf').click({ button: 'right' });
await expect(page.getByRole('menuitem', { name: 'Download' })).toBeVisible();

// Click at specific coordinates relative to the element
await page.getByRole('slider').click({ position: { x: 10, y: 0 } });

// Click while holding a modifier key
await page.getByRole('checkbox', { name: 'Item 3' }).click({ modifiers: ['Shift'] });

Hover

// Hover to reveal a tooltip or dropdown
await page.getByRole('button', { name: 'Help' }).hover();
await expect(page.getByRole('tooltip')).toBeVisible();
await expect(page.getByRole('tooltip')).toHaveText('Click for documentation');

// Hover to reveal a hidden action button in a table row
await page.getByRole('row', { name: 'Alice Johnson' }).hover();
await page.getByRole('button', { name: 'Edit' }).click();

Mouse movement

// Move mouse to absolute page coordinates
await page.mouse.move(100, 200);

// Drag from one position to another
await page.mouse.move(100, 200);
await page.mouse.down();
await page.mouse.move(300, 200, { steps: 10 }); // steps makes it smoother
await page.mouse.up();

The steps parameter for mouse.move() breaks the move into intermediate points, which matters for drag-and-drop implementations that track mousemove events along the path.

Drag and drop

For standard HTML5 drag-and-drop:

// Using dragTo — simplest approach
await page.getByText('Card A').dragTo(page.getByText('Column B'));

// With specific drop position within the target
await page.getByText('File.pdf').dragTo(page.locator('.upload-zone'), {
  targetPosition: { x: 50, y: 50 },
});

For custom drag implementations that use mousedown/mousemove/mouseup events:

const source = page.getByTestId('draggable-card');
const target = page.getByTestId('drop-zone');

const sourceBounds = await source.boundingBox();
const targetBounds = await target.boundingBox();

await page.mouse.move(sourceBounds!.x + sourceBounds!.width / 2, sourceBounds!.y + sourceBounds!.height / 2);
await page.mouse.down();
await page.mouse.move(targetBounds!.x + targetBounds!.width / 2, targetBounds!.y + targetBounds!.height / 2, { steps: 20 });
await page.mouse.up();

Scroll

// Scroll the page
await page.mouse.wheel(0, 500); // Scroll down 500px

// Scroll within a specific element
await page.getByRole('log').hover();
await page.mouse.wheel(0, 300);

// Scroll to element (use this before interacting with off-screen elements)
await page.getByTestId('submit-section').scrollIntoViewIfNeeded();

Combining keyboard and mouse

The most realistic interaction patterns combine both:

test('multi-select items with Ctrl+click', async ({ page }) => {
  await page.goto('/files');

  // Select first item
  await page.getByRole('row').nth(0).click();

  // Ctrl+click to add to selection
  await page.getByRole('row').nth(2).click({ modifiers: ['Control'] });
  await page.getByRole('row').nth(4).click({ modifiers: ['Control'] });

  // Delete selected items
  await page.keyboard.press('Delete');

  await expect(page.getByRole('row')).toHaveCount(7); // Started with 10, deleted 3
});

When to use these APIs

Most of your tests should use high-level locator methods (click(), fill(), selectOption()). Drop to keyboard/mouse APIs when:

  • Testing keyboard shortcuts or accessibility
  • Testing hover-triggered UI elements
  • Testing drag-and-drop interactions
  • Testing context menus
  • Testing rich text editors
  • Simulating complex multi-step interactions

Direct keyboard and mouse events are slower and more brittle than high-level actions. Use them only when the high-level API can't express what you need.

→ See also: Playwright Locators: getByRole, getByLabel, getByText, getByTestId Compared | Accessibility Testing with Playwright: Automated a11y Checks | File Upload and Download in Playwright Tests