selectOption() works on native
elements only: call it on a React Select or Material UI combobox built from divs and Playwright throws. Custom dropdowns need a different approach: click to open, then click the option by role or visible text. This article covers native selects, the click-open pattern for custom components, type-to-filter dropdowns, keyboard navigation, React Select's specific class structure, multi-select, and the assertion that confirms something was actually selected rather than just clicked.
Native elements
The simplest case: a standard HTML element.
``html
Country
Select a country
United States
Germany
Poland
`
`
typescript
// Select by visible text
await page.getByLabel('Country').selectOption('Germany');
// Select by value attribute
await page.getByLabel('Country').selectOption({ value: 'de' });
// Select by index (0-based, avoid this — fragile)
await page.getByLabel('Country').selectOption({ index: 2 });
// Verify selection
await expect(page.getByLabel('Country')).toHaveValue('de');
// Select multiple (for )
await page.getByLabel('Tags').selectOption(['playwright', 'typescript']);
`
selectOption()
works only on elements. For custom dropdowns, you need a different approach.
Custom dropdown components
Most modern web apps use custom dropdown components (React Select, Headless UI, Material UI) instead of native
. These look like dropdowns but behave differently under the hood.
Pattern 1: Click to open, then click the option
The most common custom dropdown pattern:
`typescript
// Custom dropdown: click to open, click to select
await page.getByRole('combobox', { name: 'Country' }).click();
await page.getByRole('option', { name: 'Germany' }).click();
// Verify
await expect(page.getByRole('combobox', { name: 'Country' })).toHaveText('Germany');
`
If the dropdown uses a
listbox role:
`typescript
await page.getByRole('button', { name: 'Country' }).click();
await page.getByRole('listbox').getByRole('option', { name: 'Germany' }).click();
`
Pattern 2: Type to filter, then select
Many custom dropdowns support search/filter:
`typescript
// Type to filter
await page.getByRole('combobox', { name: 'Country' }).fill('Ger');
// Wait for filtered results to appear
await page.getByRole('option', { name: 'Germany' }).waitFor();
// Click the option
await page.getByRole('option', { name: 'Germany' }).click();
`
Pattern 3: Keyboard navigation
Some dropdowns are keyboard-driven:
`typescript
await page.getByLabel('Country').focus();
await page.keyboard.press('ArrowDown'); // Open dropdown
await page.keyboard.press('ArrowDown'); // Move to first option
await page.keyboard.press('ArrowDown'); // Move to second option
await page.keyboard.press('Enter'); // Select
`
Or type to jump to matching options:
`typescript
await page.getByLabel('Country').focus();
await page.keyboard.type('G'); // Jump to Germany
await page.keyboard.press('Enter');
`
Inspecting the actual dropdown implementation
Before writing the test, inspect the element in DevTools:
1. Right-click the dropdown → Inspect
2. Check the element type:
or a /?
3. If custom, interact with it manually and watch the DOM changes
4. Check
role attribute or ARIA roles
The right testing approach depends entirely on the implementation. Don't assume.
React Select (a common library)
React Select is one of the most used dropdown libraries. It has specific behavior:
`typescript
// React Select combobox
const dropdown = page.locator('.react-select__control');
await dropdown.click();
// Type to filter
await dropdown.locator('input').fill('Ger');
await page.locator('.react-select__option', { hasText: 'Germany' }).click();
// Verify
await expect(page.locator('.react-select__single-value')).toHaveText('Germany');
// Clear selection
await page.locator('.react-select__clear-indicator').click();
`
React Select uses specific CSS classes. If your version generates different class names, adapt accordingly. Prefer
getByRole when possible:
`typescript
// Better — uses semantic roles
await page.getByRole('combobox', { name: 'Country' }).click();
await page.getByRole('option', { name: 'Germany' }).click();
`
Multi-select dropdowns
`typescript
// Native
await page.getByLabel('Skills').selectOption(['playwright', 'typescript', 'cicd']);
// Custom multi-select: click multiple options
await page.getByRole('button', { name: 'Skills' }).click();
await page.getByRole('option', { name: 'Playwright' }).click();
await page.getByRole('option', { name: 'TypeScript' }).click();
await page.getByRole('option', { name: 'CI/CD' }).click();
// Close the dropdown
await page.keyboard.press('Escape');
// Verify selections
await expect(page.getByTestId('selected-skills')).toContainText('Playwright');
await expect(page.getByTestId('selected-skills')).toContainText('TypeScript');
`
Date dropdowns (day, month, year selectors)
Date inputs with separate month/day/year dropdowns:
`typescript
await page.getByLabel('Month').selectOption('June');
await page.getByLabel('Day').selectOption('15');
await page.getByLabel('Year').selectOption('1990');
`
For
:
`typescript
// Works cross-browser
await page.getByLabel('Date of birth').fill('1990-06-15');
// Verify
await expect(page.getByLabel('Date of birth')).toHaveValue('1990-06-15');
`
Verifying dropdown options
`typescript
// Verify all options are present
const options = await page.getByLabel('Country').locator('option').allTextContents();
expect(options).toContain('Germany');
expect(options).toContain('Poland');
// Verify a specific option is disabled
await expect(page.getByRole('option', { name: 'Out of stock item' })).toBeDisabled();
// Verify option count
const optionCount = await page.getByLabel('Category').locator('option').count();
expect(optionCount).toBeGreaterThan(0);
`
Common mistakes
Using selectOption() on a custom dropdown: Works only on `. Custom dropdowns require click or keyboard interactions.
Not waiting for options to appear : After opening a dropdown that loads options asynchronously, wait for the option to be visible before clicking.
Selecting by index : Fragile — a new option added before yours breaks the test. Use text or value instead.
Not checking what the dropdown shows after selection : Assert the selected value is correct, not just that the click didn't throw an error.
→ See also: Playwright Locators: getByRole, getByLabel, getByText, getByTestId Compared | Playwright Assertions: The Complete Guide | Multi-Tab, Multi-Window, and iFrame Handling in Playwright