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.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
| Feature | Introduced | Why it matters |
|---|---|---|
| W3C WebDriver protocol only | 4.0 | Same behaviour in every browser; no more JSON Wire quirks |
| Relative locators | 4.0 | Find elements by visual position when the DOM has no stable attribute |
| Native window and tab creation | 4.0 | newWindow() instead of JavaScript hacks |
| Element screenshots | 4.0 | Capture a single element, not the whole viewport |
| Shadow DOM access | 4.0 | getShadowRoot() reaches into web components |
| Chrome DevTools Protocol (CDP) | 4.0 | Network throttling, geolocation, console logs on Chromium |
| Redesigned Grid | 4.0 | Standalone, hub-and-node or fully distributed; Docker-first |
| Selenium Manager | 4.6 | Automatic driver (and browser) management; the end of “chromedriver not found” |
| Browser downloads via Selenium Manager | 4.11 | Pin exact Chrome for Testing, Firefox or Edge versions |
| Java 8 dropped (Java 11 minimum) | 4.14 | Native HTTP client; smaller dependency tree |
| WebDriver BiDi high-level APIs | 4.1x onward | Console logs, JS errors, auth, network interception across browsers |
| Firefox CDP removed | 4.29 | BiDi is the only bidirectional path on Firefox |
| Python 3.9 dropped | 4.37 | Minimum is Python 3.10 |
| Screen and network emulation via BiDi | 4.40, 4.41 | Orientation, network conditions, touch, viewport, media features |
| Kubernetes-native Dynamic Grid, session events | 4.41 | Pods on demand, fireSessionEvent, event-driven video |
| Selenium CLI tool, signed .NET assemblies | 4.44 | One binary for browser and driver management; enterprise-friendly NuGet |
| Electron app support (Java) | 4.45 | ElectronOptions and ElectronDriver for desktop apps |
| Redis-backed Grid session state | 4.44, 4.45 | Highly available Distributor, SessionMap and SessionQueue |
| Safari BiDi (Technology Preview) | 4.47 | BiDi 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.
import static org.openqa.selenium.support.locators.RelativeLocator.with;
WebElement password = driver.findElement(By.id("password"));
// The text input directly above the password fieldWebElement 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 Byfrom selenium.webdriver.support.relative_locator import locate_with
password = driver.find_element(By.ID, "password")
# The text input directly above the password fieldemail = driver.find_element(locate_with(By.TAG_NAME, "input").above(password))
# Chain constraintssubmit = 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 fieldconst email = await driver.findElement(locateWith(By.tagName('input')).above(password));
// Chain constraintsconst 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 fieldIWebElement email = driver.FindElement( RelativeBy.WithLocator(By.TagName("input")).Above(password));
// Chain constraintsIWebElement 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.
import org.openqa.selenium.WindowType;
// Opens a new tab and switches to itdriver.switchTo().newWindow(WindowType.TAB);driver.get("https://example.com/help");
// Opens a new window and switches to itdriver.switchTo().newWindow(WindowType.WINDOW);# Opens a new tab and switches to itdriver.switch_to.new_window("tab")driver.get("https://example.com/help")
# Opens a new window and switches to itdriver.switch_to.new_window("window")// Opens a new tab and switches to itawait driver.switchTo().newWindow('tab');await driver.get('https://example.com/help');
// Opens a new window and switches to itawait driver.switchTo().newWindow('window');// Opens a new tab and switches to itdriver.SwitchTo().NewWindow(WindowType.Tab);driver.Navigate().GoToUrl("https://example.com/help");
// Opens a new window and switches to itdriver.SwitchTo().NewWindow(WindowType.Window);Element Screenshots and Shadow DOM
Two small 4.0 additions that remove a lot of JavaScript from older suites:
// Screenshot of one elementWebElement 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 componentWebElement host = driver.findElement(By.cssSelector("date-picker"));SearchContext shadow = host.getShadowRoot();WebElement day = shadow.findElement(By.cssSelector("input.day"));# Screenshot of one elementchart = driver.find_element(By.ID, "revenue-chart")chart.screenshot("revenue-chart.png")
# Reach inside a web componenthost = driver.find_element(By.CSS_SELECTOR, "date-picker")shadow = host.shadow_rootday = shadow.find_element(By.CSS_SELECTOR, "input.day")const fs = require('fs');
// Screenshot of one elementconst chart = await driver.findElement(By.id('revenue-chart'));fs.writeFileSync('revenue-chart.png', await chart.takeScreenshot(), 'base64');
// Reach inside a web componentconst 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 elementIWebElement chart = driver.FindElement(By.Id("revenue-chart"));((ITakesScreenshot)chart).GetScreenshot().SaveAsFile("revenue-chart.png");
// Reach inside a web componentIWebElement 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.
// No System.setProperty("webdriver.chrome.driver", ...) neededWebDriver 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 neededdriver = webdriver.Chrome()
# Ask Selenium Manager for a specific browser buildoptions = 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 neededconst driver = await new Builder().forBrowser(Browser.CHROME).build();
// Ask Selenium Manager for a specific browser buildconst options = new chrome.Options().setBrowserVersion('128');const pinned = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build();// No driver path neededIWebDriver driver = new ChromeDriver();
// Ask Selenium Manager for a specific browser buildvar 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:
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 = Truedriver = 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 objectvar 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
KubernetesSessionFactorystarts 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 withSE_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):
NodeCommandInterceptorplugs custom logic into every command via--ext. - Images on GHCR (April 2026): pull from
ghcr.io/seleniumhqwhen 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:
| Removed | Version | Replacement |
|---|---|---|
| JSON Wire Protocol | 4.0 | W3C WebDriver (automatic) |
DesiredCapabilities as the primary API | 4.0 | Browser Options classes |
driver.findElementByXxx() (Java) | 4.0 | driver.findElement(By.xxx()) |
executable_path argument (Python) | 4.10 | Service(executable_path=...) or Selenium Manager |
| Java 8 | 4.14 | Java 11+ |
| Firefox CDP | 4.29 | BiDi |
| Python 3.9 | 4.37 | Python 3.10+ |
FirefoxBinary, FTP proxy (Python) | 4.40 | options.binary_location |
| Legacy logging classes (Java) | 4.45 | BiDi 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.