WebKit native autofocus workaround
Upstream reports: WebKit bug 308393 and Playwright issue 43086. See also the proposed WebKit fix and standalone reproduction.
Playwright's bundled WebKit can apply native HTML autofocus after another field
has received focus. If this happens during locator.fill(), text can land in the
wrong field. @lullabot/playwright-testing exports
suppressWebKitAutofocus() for installing a workaround in your own browser
contexts. You can keep using Playwright's bundled browser.
Install in your context fixture
The utility runs in the browser through BrowserContext.addInitScript(); do not
call it directly in Node.js. Install it before navigating any pages, and only
when browserName === 'webkit'. This includes desktop Safari and mobile device
profiles; project names do not affect selection. Importing the package alone
installs nothing and leaves native autofocus unchanged.
The package does not export a shared test fixture. Extend Playwright's fixture
locally if you want automatic installation and a per-test opt-out:
import { test as base, expect } from '@playwright/test';
import { suppressWebKitAutofocus } from '@lullabot/playwright-testing';
interface AutofocusOptions {
webkitAutofocusWorkaround: boolean;
}
const test = base.extend<AutofocusOptions>({
webkitAutofocusWorkaround: [true, { option: true }],
context: async ({ context, browserName, webkitAutofocusWorkaround }, use) => {
if (browserName === 'webkit' && webkitAutofocusWorkaround) {
await context.addInitScript(suppressWebKitAutofocus);
}
await use(context);
},
});
export { test, expect };
Chromium and Firefox retain their normal autofocus behavior because this fixture skips installation in those browsers. The helper itself does not select a browser. For manually created WebKit contexts, install it explicitly before navigation:
const context = await browser.newContext();
await context.addInitScript(suppressWebKitAutofocus);
const page = await context.newPage();
await page.goto('/login');
@lullabot/playwright-drupal installs the same helper by default in its shared
fixture. See its configuration and opt-out guide.
What changes in your tests
Native document autofocus is suppressed. Elements with autofocus briefly
become unfocusable while WebKit discards their pending autofocus requests. Normal
form controls become invisible but keep their layout boxes; other HTML candidates
and their descendants temporarily leave layout. Their authored styles are restored
afterward, and the autofocus attribute is retained.
Ordinary Playwright actions wait for these elements to become visible. The
workaround leaves Locator.fill(), focus methods, selection, keyboard input,
input events, disabled/read-only checks and timeouts unchanged. However, application
code that measures or focuses an affected element during loading can observe the
temporary hiding. Slow stylesheets and background-page rendering can extend it.
Opt out when testing autofocus or loading behavior
Skip installation for tests that exercise native autofocus, dialog/popover autofocus, initial scripted focus, or layout while the page is loading. With the local fixture above, use:
test.describe('native autofocus behavior', () => {
test.use({ webkitAutofocusWorkaround: false });
test('login field receives native focus', async ({ page }) => {
await page.goto('/login');
await expect(page.getByLabel('Username')).toBeFocused();
});
});
For configuration-level options with that local fixture, use
defineConfig<AutofocusOptions> and set use.webkitAutofocusWorkaround to false.
The option is defined by your fixture, not registered by the generic package.
Set it before the browser context is created; changing it afterward cannot remove
an installed init script.
Coverage and limitations
The workaround protects standard light-DOM HTML forms in the browser context
where it is installed. Coverage includes navigations, reloads, popups,
context.newPage(), same-origin frame navigations, srcdoc, and dynamic form
insertion, replacement and reinsertion.
It handles queued candidates even if application code removes their autofocus
attribute or temporarily detaches them before the queue is processed.
Keep these boundaries in mind when deciding whether a test should opt out:
- Contexts created separately with
browser.newContext()do not inherit the workaround. Install it explicitly in each context that needs it. Tests usingtestdirectly from@playwright/testdo not install it automatically. - Cross-origin frames are left alone because WebKit already rejects their native document autofocus candidates.
- Application JavaScript can still steal focus during a fill. The reproduction's controlled JavaScript focus-stealing case remains observable with the workaround enabled; it is separate from the native autofocus race.
- SVG autofocus, shadow-root internals, synchronous dialog/popover focusing, and CSS transitions or animations that keep a hidden candidate focusable are outside the validated scope.
force: true, direct keyboard input and page scripts can bypass the visibility wait. Wait for an autofocus candidate to become visible before using them.- An application style change to exactly the temporary
visibility: hidden !importantordisplay: none !importantvalue cannot be distinguished from the workaround's own value during that interval.
Validation
The original implementation was validated on Debian 13 with Playwright 1.63.0 and bundled WebKit 26.6 (build 2359). The standalone reproduction, using the shared fixture, completed 2,000 desktop native navigations without a wrong-field fill. The iPhone 13 project also completed 2,000 native navigations. Both runs suppressed native autofocus before filling. Chromium and Firefox retained native autofocus and passed 300 repeated navigations each.
The original browser suite passed 44 tests covering navigation, frames, popups,
dynamic forms, blocked stylesheets, authored styles, input behavior and opt-out. Native
stress runs can also pass without the workaround because the race is intermittent;
queue-specific regressions verify suppression separately. The controlled
JavaScript route still produces Username alicesecret and an empty Password.
Desktop Safari and iPhone 13 here use Playwright's Linux WebKit and device emulation. macOS, Windows, system Safari, physical iPhones and older Playwright bundles were not tested. Check your tests on the host and bundled browser versions you support when adopting the workaround or upgrading Playwright.
Run the generic browser regressions from the monorepo root with:
The root npm run test:autofocus command also runs the Drupal fixture integration
checks. Both suites use bundled Chromium, Firefox, desktop WebKit, and iPhone 13
emulation. Set REPEATS to change the native navigation stress count (default 300).
When the workaround can be removed
Keep the workaround enabled until the WebKit bundled with your Playwright version
includes the upstream fix and your native autofocus tests pass with
webkitAutofocusWorkaround: false. A merged WebKit PR alone does not establish
that your bundled browser is fixed.
The package can remove the workaround once its minimum supported Playwright version includes the fix and native stress, focus-preservation, dynamic-form and frame tests pass without it on supported platforms and desktop/mobile profiles.
Implementation details
WebKit queues autofocus candidates on insertion and does not recheck the
autofocus attribute when processing them. Removing the attribute in a
MutationObserver therefore cannot reliably cancel the pending focus.
The workaround instead temporarily makes candidates unfocusable so WebKit discards
them. It restores their styles after the top document's DOMContentLoaded and an
animation-frame callback: WebKit flushes autofocus before these callbacks, but a
parser waiting for stylesheets can defer the flush. This uses investigated WebKit
rendering order rather than assuming that a two-frame delay guarantees safety.
Relevant source: candidate insertion,
queue processing,
and rendering order.