PRACTICAL GUIDE / ElementNotInteractableException zero size Selenium
Your locator found the hidden clone, not the live control
Use rendered geometry and DOM context to identify Selenium's zero-size match, choose the active control, and avoid clicks that bypass user behavior.
In this guide5 sections
What you will learn
- Why DOM presence is not enough
- Inspect every match, not only the first
- Tell a zero-size match from the nearby failures
- Fix the locator before adding a wait
The locator succeeds, but the click fails with element not interactable: element has zero size. DevTools shows two Save buttons: a hidden template first and the live editor second. Selenium did exactly what the locator asked and returned the wrong one.
Why DOM presence is not enough
findElement() answers a narrow question: is there a matching node in the current search context? It does not promise that the node participates in layout or accepts the requested interaction. When several nodes match, the first matching element is returned, even if a later match is the control a user can see.
A node can exist without a usable rendered box for several reasons:
- It or an ancestor has the HTML
hiddenattribute ordisplay: none. - A collapsed component keeps its old DOM subtree mounted.
- Desktop and mobile variants are both in the DOM, with CSS exposing only one.
- A framework inserted the node before layout data or content made it visible.
- The locator targeted a wrapper, hidden input, or template copy instead of the visible control.
- The element sits in a closed panel whose dimensions have animated down to zero.
The WebDriver click algorithm needs an in-view center point. Selenium's Java API describes ElementNotInteractableException as the case where an element is present but not in a state that can be interacted with, including an undisplayed element or one whose center cannot be scrolled into the viewport. With no client rectangle or a rectangle with no usable width or height, the browser cannot produce a meaningful pointer target.
This is different from click interception. An intercepted element normally has a rendered box, but another painted element owns its click point. It is also different from NoSuchElementException, where the current DOM and search context contain no match, and from StaleElementReferenceException, where a previously returned node is no longer attached to the expected document.
WebElement.getRect() is a useful first check, but it is not the whole layout story. getClientRects() reports the CSS border boxes generated for an element, while getBoundingClientRect() provides their enclosing rectangle relative to the viewport. Computed styles and ancestor state explain why those values are empty or zero.
Inspect every match, not only the first
The following JUnit 5 test reproduces the hidden-clone problem. It prints layout evidence for both matches, proves that the broad locator selects the hidden button, then scopes the fixed locator to the active editor.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.ElementNotInteractableException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
class ZeroSizeElementTest {
private final WebDriver driver = new ChromeDriver();
@AfterEach
void stopBrowser() {
driver.quit();
}
@Test
void selectsTheRenderedControlInTheActivePanel() {
String html = """
<section id="editor-template" hidden>
<button class="save">Save</button>
</section>
<section id="active-editor" data-state="open">
<button class="save"
onclick="document.querySelector('#status').textContent='saved'">
Save
</button>
<p id="status">not saved</p>
</section>
""";
String page = "data:text/html;base64," + Base64.getEncoder()
.encodeToString(html.getBytes(StandardCharsets.UTF_8));
driver.get(page);
By broad = By.cssSelector("button.save");
List<WebElement> matches = driver.findElements(broad);
assertEquals(2, matches.size());
for (int index = 0; index < matches.size(); index++) {
System.out.printf("match[%d]=%s%n", index, layoutOf(matches.get(index)));
}
assertThrows(
ElementNotInteractableException.class,
() -> driver.findElement(broad).click()
);
By activeSave = By.cssSelector(
"#active-editor[data-state='open'] button.save"
);
WebElement save = new WebDriverWait(driver, Duration.ofSeconds(2))
.ignoring(StaleElementReferenceException.class)
.until(current -> renderedElement(current, activeSave));
save.click();
assertEquals("saved", driver.findElement(By.id("status")).getText());
}
private WebElement renderedElement(WebDriver current, By locator) {
List<WebElement> candidates = current.findElements(locator);
if (candidates.size() != 1) {
return null;
}
WebElement candidate = candidates.get(0);
boolean hasUsableBox = (Boolean) ((JavascriptExecutor) current)
.executeScript("""
const el = arguments[0];
const rect = el.getBoundingClientRect();
return el.getClientRects().length > 0
&& rect.width > 0 && rect.height > 0;
""", candidate);
return hasUsableBox && candidate.isDisplayed() && candidate.isEnabled()
? candidate : null;
}
@SuppressWarnings("unchecked")
private Map<String, Object> layoutOf(WebElement element) {
return (Map<String, Object>) ((JavascriptExecutor) driver)
.executeScript("""
const el = arguments[0];
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
const hiddenOwner = el.closest('[hidden]');
const ariaHiddenOwner = el.closest('[aria-hidden="true"]');
let cssHiddenOwner = null;
for (let node = el; node; node = node.parentElement) {
const nodeStyle = getComputedStyle(node);
if (nodeStyle.display === 'none'
|| nodeStyle.visibility === 'hidden'
|| nodeStyle.visibility === 'collapse') {
cssHiddenOwner = node;
break;
}
}
return {
id: el.id || null,
connected: el.isConnected,
clientRectCount: el.getClientRects().length,
rect: {left: rect.left, top: rect.top,
width: rect.width, height: rect.height},
display: style.display,
visibility: style.visibility,
opacity: style.opacity,
hiddenAncestor: hiddenOwner ? hiddenOwner.id || hiddenOwner.tagName : null,
cssHiddenAncestor: cssHiddenOwner
? cssHiddenOwner.id || cssHiddenOwner.tagName : null,
ariaHiddenAncestor: ariaHiddenOwner
? ariaHiddenOwner.id || ariaHiddenOwner.tagName : null,
html: el.outerHTML.slice(0, 200)
};
""", element);
}
}The test uses a state-aware locator because the application exposes an active panel. The short geometry wait handles a real rendering transition and enforces exactly one candidate. Its cost is extra browser scripting on each poll. If the component has a reliable readiness attribute or accessible state, waiting on that contract is usually clearer than polling pixels.
Tell a zero-size match from the nearby failures
Run the failing method alone and keep the exception untrimmed:
mvn -Dtest=ZeroSizeElementTest#selectsTheRenderedControlInTheActivePanel \
-DtrimStackTrace=false testFor a real failure, capture evidence before a retry or page refresh. Start with the match count. A count of zero points to timing, frame, shadow-root, or locator context. A count greater than one makes duplicate variants and hidden templates the leading suspects.
Next, compare each element's DOM context. IDs and a short outerHTML fragment often reveal mobile, template, closed, or inactive containers. Walk up to the nearest component root instead of dumping the entire page, which creates noisy artifacts and may expose user data.
Geometry separates several cases:
- No client rectangles and zero dimensions usually indicate no generated CSS box, often because
display: noneorhiddenapplies in the ancestor chain. - Positive dimensions with
visibility: hiddenstill describe layout space, but the element is not visible to the user. - Positive dimensions outside the viewport may be recoverable by WebDriver scrolling, unless a clipped container prevents a usable center from entering view.
- Positive dimensions at the click point with a different topmost node indicate interception, not zero size.
- A node that becomes disconnected while being inspected indicates a re-render and should be diagnosed as stale lifecycle behavior.
Take a screenshot at the same time, but do not expect an image to show a hidden clone. The DOM evidence explains what the screenshot cannot display. Conversely, a screenshot can reveal that the intended component never opened, so the hidden result is a downstream symptom of an earlier failed action.
Viewport and media queries matter. Reproduce the CI width, height, device scale, and browser. A desktop button can be hidden while a mobile menu action is active, or the reverse. Merely maximizing the window may make the failure disappear while abandoning the viewport the suite was meant to test.
Timing can change geometry without making the element stale. A framework may keep the same DOM node while a panel collapses, so the stored WebElement remains connected even though its rectangle becomes zero between lookup and click. Record geometry immediately before the action and compare it with the failure-time snapshot. Re-locating helps only when the locator expresses the active state; re-locating the same broad selector simply returns the same hidden node again.
Inspect the ancestor chain when the element's own computed display looks normal. display: none and visibility: hidden can come from a parent, and a closed dialog or inactive tab often marks the owning region rather than every descendant. aria-hidden="true" does not remove layout by itself, but it is a strong clue that the locator entered an inactive variant. A short path to the nearest hidden or state-bearing ancestor is more useful than hundreds of computed CSS properties.
Fix the locator before adding a wait
When two variants coexist, anchor the locator to the active component. Useful contracts include an open dialog, selected tab panel, active route region, or product-owned data-testid. The cost is tighter coupling to component state, but that coupling is honest: the test needs the Save button belonging to a specific editor, not any button with that text.
If only one element exists and it gains geometry after asynchronous rendering, wait for the application state that makes it ready. A positive rectangle can be a supporting condition, not the sole product assertion. An element can have width and height while still disabled, covered, or semantically unavailable.
Reject broad fixes such as this:
driver.findElements(By.cssSelector("button.save")).stream()
.filter(WebElement::isDisplayed)
.findFirst()
.orElseThrow()
.click();It may unblock one failure, but it silently accepts two displayed Save buttons and makes DOM order part of the test. Filtering is excellent diagnostic code. Production test code should state which component owns the control and, when duplicates would be a defect, assert uniqueness.
The product may need the fix. A control that remains zero-size in a supported viewport is not automation timing. The same is true when the visible label has no usable interactive target or a collapsed panel still receives focus. Escalate with the viewport, DOM state, rectangle, and screenshot instead of teaching the test to manipulate hidden internals.
Make uniqueness part of the contract when the page should expose one active control. A wait that returns only when exactly one state-scoped match has a usable box catches both absence and accidental duplication. The stricter condition can reveal product regressions that a first-visible filter would ignore, at the cost of failing during intentional designs that show the same action in two active locations.
Responsive products sometimes do intend two usable controls, such as matching actions in a header and sticky footer. In that case, identify the one belonging to the journey by region or accessible name rather than enforcing global uniqueness. The test should encode the product decision instead of assuming every duplicate is a defect.
Do not bypass the user's interaction model
JavaScript click() can invoke an event listener on an element that has no rendered box. That proves only that a handler exists. It does not prove a keyboard, mouse, or touch user can reach the control, so it is the wrong repair for an end-to-end interaction test.
Do not remove hidden, change CSS, or assign dimensions from the test. Those mutations create a page no customer sees. They also erase the evidence that would identify a responsive-layout or component-state defect.
Some elements are intentionally not clicked. Upload tests normally send a file path to the file input according to Selenium's upload flow, even when the application presents a styled label or button. Select elements should be operated through the supported selection interaction, not by clicking a hidden option. Custom controls should be driven through their visible, accessible surface.
Avoid a geometry wait when the missing box is permanent by design. No timeout turns a hidden template into the active editor. It only delays the same diagnosis and makes the suite slower.
Finally, do not treat a larger browser window as a universal solution. Change the viewport only when the test's declared device profile was wrong. If the suite covers the smaller layout, locate and use the control that layout actually exposes. A useful fix preserves the user's path and makes the locator's ownership unmistakable.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official w3.org reference
w3.org
Primary documentation selected and verified for the claims in this guide.
- 04Official developer.mozilla.org reference
developer.mozilla.org
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Why did Selenium find an element and then say it has zero size?
Finding checks the DOM, while clicking requires a rendered, interactable target. The locator may have returned a hidden template, collapsed panel, duplicate mobile control, or element whose layout has not completed.
How can I confirm that Selenium matched a hidden duplicate?
Call findElements() with the same locator and record each match's ID, ancestors, client-rectangle count, dimensions, and computed display and visibility. Two matches with only one rendered inside the active component point to a locator problem.
Does presenceOfElementLocated mean an element can be clicked?
No. Presence only establishes that a matching node exists in the DOM. It says nothing about a rendered box, visibility, enabled state, obstruction, or whether the match belongs to the active UI.
Should I filter findElements() by isDisplayed() and click the first result?
That can help during diagnosis, but it is a weak long-term contract. A locator scoped to the open dialog or active panel is clearer and fails when the page accidentally renders two active controls.
Can JavaScript click solve an element has zero size error?
Programmatic activation may fire a handler on a control no user can reach. Fix the locator or wait for the intended layout state; if the visible UI never gains a usable box, report the product defect.
RELATED GUIDES
Continue the learning route
GUIDE 01
Drain Selenium Grid Nodes for Zero-Downtime Browser Upgrades
Upgrade Selenium Grid browser nodes without dropping active sessions by draining capacity, monitoring slots, replacing images, and verifying registration.
GUIDE 02
How to Handle Dropdowns in Selenium
Learn how to handle dropdowns in Selenium using Select, custom lists, multi-select, dynamic options, keyboard actions, and stable click patterns.
GUIDE 03
Selenium executeAsyncScript in Java
Selenium executeAsyncScript Java uses a final callback argument and the script timeout. Learn return conversion, error handling, and tested Java patterns.
GUIDE 04
Data-Driven Testing in Selenium
Learn data-driven testing in Selenium using CSV, Excel, and TestNG DataProvider patterns to run one script across many reusable test datasets.
GUIDE 05
Handle Alerts in Selenium: Complete Guide
Handle alerts in Selenium with examples for accept, dismiss, prompt text, explicit waits, unexpected alerts, browser prompts, and common mistakes in CI.