Skip to main content
SeleniumDecoded

What's New in Selenium 4

Every feature Selenium 4 added since 4.0, from relative locators and Selenium Manager to BiDi, Electron support and Kubernetes-native Grid, and which release introduced each.

Selenium 4 Stable Updated 9 Sept 2026 · Verified against Selenium 4.48.0

Selenium 4.0 shipped in October 2021, but “Selenium 4” in 2026 is a very different toolkit from the one that launched. The project releases roughly monthly, and the 4.4x series (4.40 through 4.48, January to August 2026) added things that did not exist a year earlier. This lesson is the map: what changed, when, and how to use it.

The Headline Changes at a Glance

FeatureIntroducedWhy it matters
W3C WebDriver protocol only4.0Same behaviour in every browser; no more JSON Wire quirks
Relative locators4.0Find elements by visual position when the DOM has no stable attribute
Native window and tab creation4.0newWindow() instead of JavaScript hacks
Element screenshots4.0Capture a single element, not the whole viewport
Shadow DOM access4.0getShadowRoot() reaches into web components
Chrome DevTools Protocol (CDP)4.0Network throttling, geolocation, console logs on Chromium
Redesigned Grid4.0Standalone, hub-and-node or fully distributed; Docker-first
Selenium Manager4.6Automatic driver (and browser) management; the end of “chromedriver not found”
Browser downloads via Selenium Manager4.11Pin exact Chrome for Testing, Firefox or Edge versions
Java 8 dropped (Java 11 minimum)4.14Native HTTP client; smaller dependency tree
WebDriver BiDi high-level APIs4.1x onwardConsole logs, JS errors, auth, network interception across browsers
Firefox CDP removed4.29BiDi is the only bidirectional path on Firefox
Python 3.9 dropped4.37Minimum is Python 3.10
Screen and network emulation via BiDi4.40, 4.41Orientation, network conditions, touch, viewport, media features
Kubernetes-native Dynamic Grid, session events4.41Pods on demand, fireSessionEvent, event-driven video
Selenium CLI tool, signed .NET assemblies4.44One binary for browser and driver management; enterprise-friendly NuGet
Electron app support (Java)4.45ElectronOptions and ElectronDriver for desktop apps
Redis-backed Grid session state4.44, 4.45Highly available Distributor, SessionMap and SessionQueue
Safari BiDi (Technology Preview)4.47BiDi reaches the last major engine

Relative Locators

Relative locators answer “the input above the password field” when nothing else is unique. They were the most visible 4.0 feature and remain a good last resort.

Relative locators
Selenium 4 Medium
import static org.openqa.selenium.support.locators.RelativeLocator.with;
WebElement password = driver.findElement(By.id("password"));
// The text input directly above the password field
WebElement email = driver.findElement(
with(By.tagName("input")).above(password)
);
// Chain constraints: a button below the email field and to the right of "Cancel"
WebElement submit = driver.findElement(
with(By.tagName("button"))
.below(By.id("email"))
.toRightOf(By.id("cancel"))
);
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with
password = driver.find_element(By.ID, "password")
# The text input directly above the password field
email = driver.find_element(locate_with(By.TAG_NAME, "input").above(password))
# Chain constraints
submit = driver.find_element(
locate_with(By.TAG_NAME, "button").below({By.ID: "email"}).to_right_of({By.ID: "cancel"})
)
const { By, locateWith } = require('selenium-webdriver');
const password = await driver.findElement(By.id('password'));
// The text input directly above the password field
const email = await driver.findElement(locateWith(By.tagName('input')).above(password));
// Chain constraints
const submit = await driver.findElement(
locateWith(By.tagName('button')).below(By.id('email')).toRightOf(By.id('cancel'))
);
using OpenQA.Selenium;
IWebElement password = driver.FindElement(By.Id("password"));
// The text input directly above the password field
IWebElement email = driver.FindElement(
RelativeBy.WithLocator(By.TagName("input")).Above(password)
);
// Chain constraints
IWebElement submit = driver.FindElement(
RelativeBy.WithLocator(By.TagName("button")).Below(By.Id("email")).RightOf(By.Id("cancel"))
);

Relative locators use the element’s bounding box, so they break when layout changes on small viewports. Prefer a data-testid when you can add one.

New Windows and Tabs Without JavaScript

Before Selenium 4 you opened a tab by sending Ctrl+T or executing JavaScript, then guessed which handle was new. Now the driver does it and switches focus for you.

Creating windows and tabs
Selenium 4 Stable
import org.openqa.selenium.WindowType;
// Opens a new tab and switches to it
driver.switchTo().newWindow(WindowType.TAB);
driver.get("https://example.com/help");
// Opens a new window and switches to it
driver.switchTo().newWindow(WindowType.WINDOW);
# Opens a new tab and switches to it
driver.switch_to.new_window("tab")
driver.get("https://example.com/help")
# Opens a new window and switches to it
driver.switch_to.new_window("window")
// Opens a new tab and switches to it
await driver.switchTo().newWindow('tab');
await driver.get('https://example.com/help');
// Opens a new window and switches to it
await driver.switchTo().newWindow('window');
// Opens a new tab and switches to it
driver.SwitchTo().NewWindow(WindowType.Tab);
driver.Navigate().GoToUrl("https://example.com/help");
// Opens a new window and switches to it
driver.SwitchTo().NewWindow(WindowType.Window);

Element Screenshots and Shadow DOM

Two small 4.0 additions that remove a lot of JavaScript from older suites:

Element screenshot and shadow root
Selenium 4 Stable
// Screenshot of one element
WebElement chart = driver.findElement(By.id("revenue-chart"));
File png = chart.getScreenshotAs(OutputType.FILE);
Files.copy(png.toPath(), Path.of("revenue-chart.png"), StandardCopyOption.REPLACE_EXISTING);
// Reach inside a web component
WebElement host = driver.findElement(By.cssSelector("date-picker"));
SearchContext shadow = host.getShadowRoot();
WebElement day = shadow.findElement(By.cssSelector("input.day"));
# Screenshot of one element
chart = driver.find_element(By.ID, "revenue-chart")
chart.screenshot("revenue-chart.png")
# Reach inside a web component
host = driver.find_element(By.CSS_SELECTOR, "date-picker")
shadow = host.shadow_root
day = shadow.find_element(By.CSS_SELECTOR, "input.day")
const fs = require('fs');
// Screenshot of one element
const chart = await driver.findElement(By.id('revenue-chart'));
fs.writeFileSync('revenue-chart.png', await chart.takeScreenshot(), 'base64');
// Reach inside a web component
const host = await driver.findElement(By.css('date-picker'));
const shadow = await host.getShadowRoot();
const day = await shadow.findElement(By.css('input.day'));
// Screenshot of one element
IWebElement chart = driver.FindElement(By.Id("revenue-chart"));
((ITakesScreenshot)chart).GetScreenshot().SaveAsFile("revenue-chart.png");
// Reach inside a web component
IWebElement host = driver.FindElement(By.CssSelector("date-picker"));
ISearchContext shadow = host.GetShadowRoot();
IWebElement day = shadow.FindElement(By.CssSelector("input.day"));

Selenium Manager: The Feature That Deleted a Thousand Stack Overflow Questions

From 4.6 onward, constructing a driver with no driver path triggers Selenium Manager. It inspects the installed browser, downloads the matching driver into a cache, and from 4.11 can download the browser itself. Version drift between Chrome and chromedriver, the top cause of “session not created” errors for a decade, is handled for you.

Zero-configuration driver setup
Selenium 4 Stable
// No System.setProperty("webdriver.chrome.driver", ...) needed
WebDriver driver = new ChromeDriver();
// Ask Selenium Manager for a specific browser build (downloads Chrome for Testing if absent)
ChromeOptions options = new ChromeOptions();
options.setBrowserVersion("128");
WebDriver pinned = new ChromeDriver(options);
from selenium import webdriver
# No executable_path needed
driver = webdriver.Chrome()
# Ask Selenium Manager for a specific browser build
options = webdriver.ChromeOptions()
options.browser_version = "128"
pinned = webdriver.Chrome(options=options)
const { Builder, Browser } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
// No driver path needed
const driver = await new Builder().forBrowser(Browser.CHROME).build();
// Ask Selenium Manager for a specific browser build
const options = new chrome.Options().setBrowserVersion('128');
const pinned = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build();
// No driver path needed
IWebDriver driver = new ChromeDriver();
// Ask Selenium Manager for a specific browser build
var options = new ChromeOptions { BrowserVersion = "128" };
IWebDriver pinned = new ChromeDriver(options);

The Selenium Manager lesson covers caching, proxies, offline mode and CI tips.

WebDriver BiDi: The Big One

Classic WebDriver is request-response: you ask, the browser answers. It cannot tell you that a console error just happened or let you block a request. Selenium 4 first answered that gap with CDP (Chromium only, version-locked) and then with WebDriver BiDi, a W3C standard that works in Chrome, Edge, Firefox and now Safari Technology Preview.

BiDi is the reason the 2026 release notes read the way they do: emulation overrides, network conditions, speculation, media features, generated protocol layers. Enable it with one capability and the high-level APIs appear on the driver:

Enable BiDi and capture console errors
Selenium 4 Stable
ChromeOptions options = new ChromeOptions();
options.setCapability("webSocketUrl", true);
RemoteWebDriver driver = new ChromeDriver(options);
driver.script().addConsoleMessageHandler(entry ->
System.out.println("[console." + entry.getLevel() + "] " + entry.getText()));
driver.script().addJavaScriptErrorHandler(error ->
System.err.println("[js error] " + error.getText()));
from selenium import webdriver
options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
driver.script.add_console_message_handler(
lambda entry: print(f"[console.{entry.level}] {entry.text}"))
driver.script.add_javascript_error_handler(
lambda error: print(f"[js error] {error.text}"))
const chrome = require('selenium-webdriver/chrome');
const options = new chrome.Options().enableBidi();
const driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
await driver.script().addConsoleMessageHandler((entry) =>
console.log(`[console.${entry.level}] ${entry.text}`));
await driver.script().addJavaScriptErrorHandler((error) =>
console.error(`[js error] ${error.text}`));
var options = new ChromeOptions { UseWebSocketUrl = true };
var driver = new ChromeDriver(options);
// .NET exposes BiDi through an async session object
var bidi = await driver.AsBiDiAsync();
await bidi.Log.OnEntryAddedAsync(entry =>
Console.WriteLine($"[{entry.Level}] {entry.Text}"));

Two dedicated lessons go deeper: WebDriver BiDi and Network Interception with BiDi.

Grid: From Hub-and-Node to Kubernetes-Native

Grid 4 was rewritten around a Router, Distributor, Session Map, Session Queue, Event Bus and Nodes. The 2026 releases made it cloud-native:

  • Dynamic Grid on Kubernetes (4.41): a KubernetesSessionFactory starts a browser Pod per session with no Docker daemon, inheriting the Node’s tolerations and resource limits.
  • Session events (4.41): driver.fireSessionEvent("test:failed", payload) reaches the event bus so sidecars can react. Combined with SE_UPLOAD_FAILURE_SESSION_ONLY=true, only failed-test videos are kept.
  • Redis-backed state (4.44, 4.45): Distributor, SessionMap and SessionQueue can share state through Redis for high availability.
  • Command interceptors (4.42): NodeCommandInterceptor plugs custom logic into every command via --ext.
  • Images on GHCR (April 2026): pull from ghcr.io/seleniumhq when Docker Hub throttles you.

See Selenium Grid and Grid on Docker and Kubernetes.

Electron Apps (4.45, Java)

Java gained first-class Electron support in June 2026. ElectronOptions points at the app binary and ElectronDriver drives it like Chrome, because it is Chrome underneath:

ElectronOptions options = new ElectronOptions();
options.setBinary("/Applications/MyApp.app/Contents/MacOS/MyApp");
WebDriver driver = new ElectronDriver(options);
driver.findElement(By.id("login")).click();

Other bindings can still automate Electron through ChromeOptions.setBinary pointing at the app plus a matching chromedriver, which is what ElectronDriver does for you.

Things That Went Away

Knowing what was removed saves debugging time when you upgrade an old suite:

RemovedVersionReplacement
JSON Wire Protocol4.0W3C WebDriver (automatic)
DesiredCapabilities as the primary API4.0Browser Options classes
driver.findElementByXxx() (Java)4.0driver.findElement(By.xxx())
executable_path argument (Python)4.10Service(executable_path=...) or Selenium Manager
Java 84.14Java 11+
Firefox CDP4.29BiDi
Python 3.94.37Python 3.10+
FirefoxBinary, FTP proxy (Python)4.40options.binary_location
Legacy logging classes (Java)4.45BiDi log handlers, driver service log files

Summary

  • Selenium 4 is a moving target: monthly releases, with the 4.4x line adding BiDi emulation, Kubernetes-native Grid, Electron support and a CLI.
  • Selenium Manager removed driver management from your to-do list. Relative locators, native tabs, element screenshots and shadow root access removed JavaScript hacks.
  • BiDi is where new capability lands; CDP is a Chromium-only bridge that Selenium 5 will drop.
  • Check the Releases & News page for the current version and breaking changes before every upgrade.

Related lessons