← All articles

Running a browser on your own desktop without it stealing focus

4 min read automationsdkheadless

You start an automation run on the machine you are working on, keep typing, and three seconds later your keystrokes are going into the automated browser. In a loop of 200 tasks that happens 200 times. The obvious fix is to stop drawing a window at all, and that fix costs more than it looks like it does.

Why headless is the wrong answer here

Headless is not a display setting. It is a different browser configuration, and the page can tell. A page can read the same surfaces it always reads, and some of them answer differently when there is no real window and no real compositor behind them. That is the whole subject of headless versus headful detection, and it is why an antidetect setup that runs headless is spending its fingerprint work to save a taskbar entry.

Minimising the window is not the answer either, for a plainer reason. A minimised window reports document.visibilityState === 'hidden', and browsers throttle timers and animation frames for pages in that state. Sites that defer work until the tab is visible will simply not do the work, and your automation waits on something that is never going to happen.

So the requirement is narrower than "hide it": keep a real, normally sized, visible window, and stop it from coming to the front.

What an unfocused launch does

focusWindow: false on launch() does exactly that one thing:

await ab.launch({ profile: 'task-01', focusWindow: false })

The window opens at its normal size, in the normal window stack, behind whatever you were using. It is not minimised, not moved off screen, and not headless, so nothing about the fingerprint changes. Your keyboard focus stays where it was.

The default is true, and only an explicit false changes anything. An unset option launches the way it always has, which matters because this is the sort of flag people set globally and forget.

Window stacking is decided inside the browser engine rather than by the SDK, so install the latest engine version for the profile before you rely on this.

It is on the MCP tool too, which is arguably where it matters most: an agent that opens browsers on your workstation while you work is the exact case this exists for. See giving an MCP agent a browser for the rest of that setup.

What it still does not fix

Not taking focus and not being visible are different states, and only the first is what this controls. A window sitting fully behind another window can still end up treated as not visible by the browser, with the timer throttling that implies. If your automation depends on a page's timers running at full rate, leave the window somewhere it is not completely covered, or put the run on a machine you are not using.

There is also no ordering guarantee beyond "not raised". If you launch six browsers unfocused, you get six windows behind your work, stacked in whatever order they opened.

Where you do not need this at all

On a server, in a container, or on any machine you are not sitting at, this option solves a problem you do not have. Nothing is stealing focus from you there, and the sensible setup is a machine with a display server and no human on it.

If your reason for wanting it is that you have twenty concurrent browsers on one desktop, the window stacking is not your real problem. Twenty visible windows on a workstation is a resource question, and a separate host answers it better than any flag.

And if you were only reaching for headless out of habit rather than because you need to run without a display, the honest version is that you probably do not need either option.

Checking it yourself

Start something you can type into, keep it focused, then launch with focusWindow: false. Keep typing. The characters should keep landing where you left them, with the browser window visible behind.

Then confirm the browser did not pay for it by evaluating this in the page:

await page.evaluate(() => [document.hasFocus(), document.visibilityState])

You want [false, 'visible']. hasFocus() false is the feature working. visibilityState still visible is the part that separates this from minimising the window, and it is the value worth re-checking on your own window layout.

The full option list is in the SDK docs.

Try it on the free tier.

Unlimited local profiles, no credit card. Check it against the detectors yourself.