Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Save the current window handle, perform the action that opens the tab or window, wait until the expected number of handles exists, then switch explicitly with driver.switchTo().window(handle). WebDriver does not automatically follow browser UI focus. After you finish in the child context, close it and switch back to a still-live handle.

This handle-based workflow works for links that open tabs, JavaScript pop-ups, and contexts created directly by Selenium 4.

How WebDriver identifies tabs and windows

Selenium treats each top-level browser tab or window as a browsing context identified by an opaque string called a window handle. getWindowHandle() returns the handle for the context currently controlled by the driver. getWindowHandles() returns the set of all live handles in the session. Pass one of those strings to driver.switchTo().window(...) to move WebDriver’s focus.

The handle text has no useful meaning. Do not parse it, assume it is stable between sessions, or rely on the order of a set when several tabs are open. A frame is different: an iframe requires switchTo().frame(...), while a top-level tab or window requires a window handle.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The reliable two-context pattern

For a link that opens one additional context, use this complete pattern. It waits for registration of the new context instead of racing the browser.

import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class NewWindowExample {
    public static void main(String[] args) {
        WebDriver driver = new ChromeDriver();
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

        try {
            driver.get("https://example.test");
            String original = driver.getWindowHandle();

            driver.findElement(By.linkText("Open new window")).click();
            wait.until(ExpectedConditions.numberOfWindowsToBe(2));

            for (String handle : driver.getWindowHandles()) {
                if (!handle.equals(original)) {
                    driver.switchTo().window(handle);
                    break;
                }
            }

            wait.until(ExpectedConditions.titleContains("Child"));
            driver.findElement(By.id("child-control")).click();

            driver.close();
            driver.switchTo().window(original);
            wait.until(ExpectedConditions.titleContains("Parent"));
        } finally {
            driver.quit();
        }
    }
}

Replace the example URL, link text, title fragments, and element ID with values from your application. The finally block guarantees that the whole session is released even when an assertion or element lookup fails.

Why each step matters

  1. Capture the parent first. Store getWindowHandle() before clicking. That gives you a known destination for cleanup.
  2. Trigger the event. Click the control that opens the context. A click may create a tab, a separate window, or no new context if the application blocks the action.
  3. Wait for a count. numberOfWindowsToBe(2) waits until the driver can see both contexts.
  4. Choose by difference. Iterate the handles and select the one that is not the saved parent.
  5. Wait for page state. A registered handle does not guarantee that navigation has finished. Wait for a title, URL condition, or distinctive element before interacting.
  6. Close only the finished context. close() closes the currently selected tab or window.
  7. Restore focus. Switch to the original (or another live handle) before issuing more commands.
  8. Quit at the end. quit() closes every remaining context and ends the WebDriver session.

Opening a tab or window with Selenium 4

When the test itself needs a fresh context, Selenium 4 can create and focus it directly. Because the command focuses the new context, no additional window switch is needed immediately afterward.

import org.openqa.selenium.WindowType;

String parent = driver.getWindowHandle();

driver.switchTo().newWindow(WindowType.TAB);
driver.get("https://example.test/tab-target");
// Interact with the new tab here.

driver.close();
driver.switchTo().window(parent);

driver.switchTo().newWindow(WindowType.WINDOW);
driver.get("https://example.test/window-target");

Use WindowType.TAB when you need another tab and WindowType.WINDOW when you need a separate browser window. Keep the parent handle before creating either one so cleanup is deterministic.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handling more than two contexts

With pop-ups, authentication tabs, or several test-created contexts, “the handle at index 1” is unsafe. The set has no contract that makes index 1 the desired target, and another tab may already exist.

Identify a target by page properties

Switch through candidates and inspect a property that distinguishes the page. A title, URL, or unique element is more robust than handle order.

String target = null;
for (String candidate : driver.getWindowHandles()) {
    driver.switchTo().window(candidate);
    if (driver.getTitle().contains("Payment")
            || driver.getCurrentUrl().contains("/checkout")) {
        target = candidate;
        break;
    }
}
if (target == null) {
    throw new IllegalStateException("Payment window was not found");
}
// driver is already switched to target

For a newly opened context, record the set before the action and subtract it from the set afterward. This remains reliable even when more than one pre-existing context is present.

Set<String> before = new HashSet<>(driver.getWindowHandles());
driver.findElement(By.cssSelector("button.open-popup")).click();
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(d -> d.getWindowHandles().size() > before.size());

Set<String> after = driver.getWindowHandles();
after.removeAll(before);
if (after.size() != 1) {
    throw new IllegalStateException("Expected one new context, found " + after.size());
}
driver.switchTo().window(after.iterator().next());

The lambda wait checks an observable condition: the number of contexts has increased. If an action legitimately opens several contexts, adapt the expected size and identify each candidate by its page state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choosing the right wait

Wait for a window count

Use ExpectedConditions.numberOfWindowsToBe(n) when the expected count is known. It is the simplest synchronization for a single pop-up and prevents a premature call to getWindowHandles().

Wait for a title or URL

After switching, wait for a title or URL fragment when navigation is the important milestone. This catches cases where the handle exists but the document is still loading.

Wait for a distinctive element

A unique element is often the strongest signal for single-page applications, redirects, and pages whose title never changes. Combine the window-count wait with an element wait in the selected context.

wait.until(ExpectedConditions.numberOfWindowsToBe(2));
String child = driver.getWindowHandles().stream()
    .filter(h -> !h.equals(original))
    .findFirst()
    .orElseThrow(() -> new IllegalStateException("No child context"));
driver.switchTo().window(child);
wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.cssSelector("[data-page='child']")));

Closing a child and returning to the parent

driver.close() affects only the currently selected context. It does not choose another tab for you. Immediately switch to a handle that remains alive:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
String child = driver.getWindowHandle();
driver.close();
if (driver.getWindowHandles().contains(original)) {
    driver.switchTo().window(original);
} else {
    throw new IllegalStateException("Parent context was already closed");
}

If you send another command while WebDriver is still attached to the closed context, the next operation can fail with NoSuchWindowException. Use quit() only when the entire test session is complete; it closes every context and cannot be undone within that session.

Common failures and precise fixes

The element in the pop-up cannot be found

Cause: the driver is still attached to the original handle. Fix: wait for the count, select the non-parent handle, then locate the element.

The test fails intermittently

Cause: the test reads handles or page state before the browser registers the new context or completes navigation. Fix: use an explicit window-count wait followed by a title, URL, or element wait. Avoid arbitrary sleeps as the primary synchronization.

NoSuchWindowException appears after cleanup

Cause: the active context was closed and the test issued another command without switching. Fix: switch to a verified live handle immediately after close().

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The wrong tab is selected

Cause: code assumes that the new handle is at index 1, or several contexts already existed. Fix: compare with a saved set and identify the target by title, URL, or a distinctive element.

The test confuses an iframe with a window

Cause: an iframe is an embedded document inside the same top-level context. Fix: use driver.switchTo().frame(...) for the frame and driver.switchTo().window(handle) for a tab or window. Return from a frame with driver.switchTo().defaultContent() before handling page-level elements.

The click opens no new context

Cause: a browser policy, application condition, blocked pop-up, or failed click prevented creation. Fix: verify the click’s preconditions, wait for the expected count with a useful timeout, and capture diagnostic information such as the current URL and page screenshot when the wait expires. Do not switch to a handle that does not exist.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and test design

  • Keep one explicit parent handle per test flow rather than repeatedly guessing which tab is “main.”
  • Use the smallest meaningful wait timeout, but allow for the slowest environment in which the test runs.
  • Prefer page-state conditions over fixed delays; they shorten fast runs and remain correct on slower runs.
  • Close temporary contexts as soon as their assertions finish to reduce resource use and ambiguity.
  • Give each context a purpose in the test and identify it by a stable page property when several are open.
  • Put cleanup in finally or your test framework’s teardown so failures do not leave browser processes behind.

Or skip the browser setup

If your goal is a rendered screenshot rather than interactive WebDriver control, ScreenshotNeo returns an image or PDF from one request. It accepts the cookie or consent banner before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

See the ScreenshotNeo API documentation for parameters. A minimal cURL call is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every plan includes its features. The Free plan provides 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it without a card.

Frequently Asked Questions

Can I switch by browser tab title without storing a handle?

You can inspect the title only after switching to a candidate handle. Store handles and use the title or URL to decide which candidate is the target; the title itself is not a replacement for a handle.

Does Selenium 4 automatically focus a tab opened by a click?

No. The browser may visually focus it, but WebDriver still requires an explicit switch to the new handle.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I use close() or quit() in teardown?

Use close() for one finished child context when other contexts must remain. Use quit() once the whole WebDriver session is finished.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.