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 MacKey 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