ScreenshotNeo

BlogHow-to

How to Click a Radio Button with Playwright

Use Playwright’s accessible locators and check() to select native radio buttons reliably, verify state, handle custom controls, frames, and failures.

By the ScreenshotNeo team1 October 20267 min read

For a native HTML radio button, locate it by its accessible role or label and call check(). Then assert the resulting state with toBeChecked().

import { test, expect } from '@playwright/test';

test('selects a plan', async ({ page }) => {
  await page.goto('/signup');

  const monthly = page.getByRole('radio', { name: 'Monthly' });
  await monthly.check();
  await expect(monthly).toBeChecked();
});

Playwright’s input guide describes setChecked (exposed as check() in the language bindings) as the easiest way to check a checkbox or radio button. The operation waits for actionability, scrolls the control into view, performs the interaction, and verifies that the state changed. See the official input documentation and locator guide.

1. Choose a reliable locator

Role and accessible name (preferred)

await page.getByRole('radio', { name: 'Monthly' }).check();

The role locator models how a user or assistive technology perceives the control. Supplying name prevents Playwright from matching every radio in the group.

Associated label

await page.getByLabel('XL').check();

This works when the label is associated with the input using for/id, or when the input is nested inside the label:

<label for="size-xl">XL</label>
<input id="size-xl" name="size" type="radio" value="xl">

CSS and test IDs

await page.locator('input[type="radio"][value="monthly"]').check();
await page.getByTestId('plan-monthly').check();

Use these when accessible markup cannot identify the control. Scope them to the relevant form or component if the page contains repeated controls.

2. Complete Playwright Test example

import { test, expect } from '@playwright/test';

test('selects and submits a billing plan', async ({ page }) => {
  await page.goto('https://example.com/signup');

  const billing = page.getByRole('group', { name: 'Billing frequency' });
  const monthly = billing.getByRole('radio', { name: 'Monthly' });
  const yearly = billing.getByRole('radio', { name: 'Yearly' });

  await monthly.check();
  await expect(monthly).toBeChecked();
  await expect(yearly).not.toBeChecked();

  await page.getByRole('button', { name: 'Continue' }).click();
});

A radio group normally allows only one checked value. Assert the selected option and, when useful, assert that a competing option is not selected.

3. check() versus click()

Use check() for a native <input type="radio">. It expresses the intended state change and includes a checked-state verification. Use click() when the user-visible target is a custom control or when the test specifically needs to exercise a click handler.

// Native radio input
await page.getByRole('radio', { name: 'Monthly' }).check();

// A custom element that behaves like a button
await page.locator('[data-plan="monthly"]').click();

If check() says the element is not a checkbox or radio, inspect the DOM. A styled div is not a native radio input even if it looks like one.

4. Verify the selection

const monthly = page.getByRole('radio', { name: 'Monthly' });
await monthly.check();
await expect(monthly).toBeChecked();

Web-first assertions wait for the condition instead of reading a value once and racing the page. For a form submission, also verify the resulting UI or request:

await expect(page.getByText('Monthly plan selected')).toBeVisible();
await expect(page).toHaveURL(/\/checkout/);

5. Scope locators for repeated groups

const shipping = page.getByRole('group', { name: 'Shipping speed' });
await shipping.getByRole('radio', { name: 'Express' }).check();

Chaining keeps a locator unique when several components contain radios with the same accessible name. A strictness error means the locator matched more than one element. Add a name, scope to a fieldset or component, or filter by a distinguishing property. Reserve first(), last(), and nth() for cases where position is part of the contract; positional tests can silently target the wrong control after a layout change.

6. Radios inside an iframe

const payment = page.frameLocator('#payment-frame');
const monthly = payment.getByRole('radio', { name: 'Monthly' });
await monthly.check();
await expect(monthly).toBeChecked();

Build the locator from frameLocator(); a page-level locator cannot reach into an iframe. If the frame is created dynamically, wait for its selector or use a frame-aware locator that matches the final frame.

7. Dynamic rendering and React-style components

Keep a Locator and resolve it at action time rather than caching an element handle. Playwright re-resolves locators after rerenders:

const choice = page.getByRole('radio', { name: 'Pro' });
await page.getByRole('button', { name: 'Load plans' }).click();
await choice.check();

If the component replaces the input after every change, wait for the relevant state with an assertion rather than adding an arbitrary sleep.

8. Custom ARIA radios

Some design systems implement radios with elements such as <div role="radio" aria-checked="false">. If the component exposes a correct ARIA role and accessible name, locate it by role and click it:

const pro = page.getByRole('radio', { name: 'Pro' });
await pro.click();
await expect(pro).toHaveAttribute('aria-checked', 'true');

check() is for native checkbox and radio inputs. For custom widgets, follow the component’s interaction contract and verify its ARIA state or visible result.

9. Other Playwright language bindings

Python

from playwright.sync_api import Playwright, sync_playwright, expect

def run(playwright: Playwright) -> None:
    browser = playwright.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com/signup")
    monthly = page.get_by_role("radio", name="Monthly")
    monthly.check()
    expect(monthly).to_be_checked()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

Java

import com.microsoft.playwright.*;

public class RadioExample {
  public static void main(String[] args) {
    try (Playwright pw = Playwright.create()) {
      Browser browser = pw.chromium().launch();
      Page page = browser.newPage();
      page.navigate("https://example.com/signup");
      Locator monthly = page.getByRole(AriaRole.RADIO,
          new Page.GetByRoleOptions().setName("Monthly"));
      monthly.check();
      assert monthly.isChecked();
      browser.close();
    }
  }
}

.NET

using Microsoft.Playwright;

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com/signup");
var monthly = page.GetByRole(AriaRole.Radio,
    new() { Name = "Monthly" });
await monthly.CheckAsync();
await Expect(monthly).ToBeCheckedAsync();

10. Troubleshooting

Error or symptom Cause Fix
Strict mode violation The locator matches multiple radios. Add the accessible name, scope to a group, or filter by value. Avoid positional selectors unless position is intentional.
getByLabel() finds nothing The label is not associated with the input. Match the label’s for value to the input id, nest the input in the label, or use a role, CSS, or test ID locator.
check() says the element is not a radio The control is a custom element rather than an input. Use the custom control’s locator and click(); assert aria-checked or the resulting UI.
Element is covered or not actionable A popup, animation, overlay, or consent banner blocks the radio. Wait for the overlay to disappear, close it through its UI, or remove the cause in test setup. Do not use force by default because it can hide a real user-facing defect.
Radio is inside an iframe The page locator cannot cross frame boundaries. Use page.frameLocator('selector'), then apply the role or label locator.
Selection disappears after a rerender The component recreated the control or reset form state. Keep a locator, wait for the state with an assertion, and inspect the component’s controlled value and event handling.
Click works but state is unchanged The custom widget does not update its state or the wrong element was clicked. Target the interactive element, inspect keyboard and pointer handlers, and assert the postcondition.

11. Reliability and performance checklist

  • Prefer getByRole('radio', { name }) or getByLabel().
  • Give every radio group a clear accessible name where possible.
  • Scope locators to the form, fieldset, dialog, or component that owns the choice.
  • Use web-first assertions such as toBeChecked().
  • Keep locators instead of stale element handles during dynamic updates.
  • Use a frame locator for iframe content.
  • Use explicit waits for meaningful conditions, not fixed delays.
  • Keep browser contexts isolated when tests run in parallel.
  • Capture a trace or screenshot on failure to diagnose overlays and rerenders.

Role and label locators usually survive CSS and layout changes better than long CSS or XPath chains. They can still fail when the accessible name changes, so treat user-facing labels as part of the tested contract.

12. Or skip the browser setup

If your goal is to document or visually verify the page after selecting a radio, ScreenshotNeo can return a screenshot with one GET request. The API accepts options for custom JavaScript, clicks, waits, selectors, device presets, full-page capture, and more; see the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/signup -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/signup"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/signup' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

13. FAQ

Can I use click() on a native radio?

Yes, but check() communicates the intended state and verifies that the native control became checked.

How do I select by value?

await page.locator('input[type="radio"][value="annual"]').check();

Can two radios in one group be checked?

Native radios sharing the same name allow one checked value. If two appear selected, inspect whether they belong to different groups or are custom widgets.

Should I use force: true?

Only when the obstruction is intentional and understood. Forced actions bypass actionability checks and can conceal a broken overlay or layout.