How to Use the @FindBy Annotation in Selenium with Java
Learn how to declare and initialize Selenium @FindBy fields in Java Page Objects, choose locators, work with lists, and troubleshoot common issues.
Use Selenium’s @FindBy annotation to declare how a Page Object locates a WebElement or List<WebElement>, then call PageFactory.initElements(driver, this) to initialize the fields. PageFactory creates proxies that normally find the element when you first call a method on the field, and repeat the lookup on later calls.
1. Add a working Page Object
Here is a complete Java class using explicit ID and CSS locators. It assumes Selenium Java is already on your project’s classpath and that the driver has been created and navigated to the login page.
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class LoginPage {
@FindBy(id = "username")
private WebElement username;
@FindBy(id = "password")
private WebElement password;
@FindBy(css = "button[type='submit']")
private WebElement submitButton;
@FindBy(css = "form .validation-error")
private List<WebElement> validationErrors;
public LoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
public void signIn(String user, String secret) {
username.sendKeys(user);
password.sendKeys(secret);
submitButton.click();
}
public int validationErrorCount() {
return validationErrors.size();
}
}
Use it after navigating the driver to the page:
LoginPage loginPage = new LoginPage(driver);
loginPage.signIn("alice", "example-password");
The selectors must match the application’s actual DOM. This example illustrates the PageFactory pattern; it does not claim to have been executed against a particular site.
2. Initialize the fields with PageFactory
- Import
FindBy,PageFactory,WebElement, andWebDriver. - Declare a field for one element as
WebElement, or a repeated set asList<WebElement>. - Put one locator declaration on each field.
- Call
PageFactory.initElements(driver, this)in the page object’s constructor, after the driver is available. - Use the fields through ordinary WebElement methods such as
click(),sendKeys(),getText(), andisDisplayed().
The annotation declares a locator; by itself, it does not populate the field. PageFactory’s documented workflow decorates WebElement and list fields with proxies. Lookup is lazy, so construction of the page object does not necessarily mean the browser has already searched for every element.
3. Choose a locator strategy
The concise annotation attributes map to Selenium locator strategies. Pick a locator that matches the page markup and is readable and maintainable for your application.
| Annotation | Example | Use when |
|---|---|---|
id |
@FindBy(id = "username") |
The element has a suitable ID. |
name |
@FindBy(name = "email") |
A name attribute identifies the control. |
css |
@FindBy(css = "button[type='submit']") |
A CSS selector expresses the target clearly. |
className |
@FindBy(className = "primary") |
A single class name identifies the target. Do not pass multiple classes as one value. |
tagName |
@FindBy(tagName = "button") |
The tag is sufficiently specific, often within a scoped component. |
linkText |
@FindBy(linkText = "Continue") |
The complete visible link text is appropriate. |
partialLinkText |
@FindBy(partialLinkText = "Contin") |
A stable substring of link text is suitable. |
xpath |
@FindBy(xpath = "//button[@type='submit']") |
The relationship or condition is clearer in XPath. |
The explicit equivalent of @FindBy(id = "username") is:
import org.openqa.selenium.support.How;
@FindBy(how = How.ID, using = "username")
private WebElement username;
Use one style consistently where it improves readability. Neither CSS nor XPath is universally best: prefer a stable application attribute and a selector that remains understandable when the page changes.
4. Find one element or a list
A WebElement field represents one located element. A List<WebElement> represents the elements matching a locator, which is useful for repeated rows, links, cards, or validation messages.
@FindBy(css = "ul.results > li")
private List<WebElement> results;
public int resultCount() {
return results.size();
}
Prefer an explicit locator for list fields. The older Selenium project wiki notes limitations in default field-name behavior for lists; explicit annotation avoids relying on that convention. An empty list can be a valid result if nothing matches, so distinguish that case from a locator that should have matched content.
5. Understand defaults, lookup timing, and caching
If a field has no recognized locator annotation, PageFactory’s annotation processor can use the Java field name as an ID or name locator. Treat that as a convenience for fields whose names really correspond to markup; use @FindBy explicitly when the intended strategy or target is not obvious.
By default, the proxy performs the lookup each time a method is called on the field. This can find a replacement element after a page update, but it also means repeated interactions can trigger repeated lookups. @CacheLookup opts into caching the located element:
import org.openqa.selenium.support.CacheLookup;
@FindBy(id = "site-logo")
@CacheLookup
private WebElement logo;
Cache only when the element is stable for the relevant lifetime. If the DOM replaces it, a cached reference may become stale. Do not add caching as a general speed fix without considering navigation and dynamic rendering.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Field is null |
The Page Object was constructed without PageFactory initialization, or the field was not decorated. | Call PageFactory.initElements(driver, this) during construction, or initialize the object using an appropriate PageFactory overload. |
NoSuchElementException on interaction |
The locator matches nothing when the proxy is used, or the page has not reached the expected state. | Check the selector against the current DOM and wait for the application state before interacting. |
StaleElementReferenceException |
The DOM changed after the element was located, or a cached element no longer refers to a live node. | Wait for the updated state and locate again; avoid caching elements that are replaced. |
| List is empty | No elements matched at lookup time, or the list selector is incorrect. | Verify the selector and page state. Use an explicit locator for lists and handle a legitimately empty result. |
IllegalArgumentException during decoration |
More than one of @FindBy, @FindBys, and @FindAll is present on the same field. |
Keep only the intended recognized locator annotation on that field. |
| Selector compiles but finds the wrong node | The selector is valid but too broad or based on unstable markup. | Scope it to a stable component and confirm it identifies the intended element, not merely any matching node. |
7. Reliability and performance considerations
- Wait for application state: PageFactory locates elements when fields are used; it does not itself guarantee that asynchronous content has finished loading. Coordinate interactions with the page’s readiness conditions.
- Repeated method calls: Default lookup on use can mean repeated locator work. Keep selectors straightforward and avoid caching dynamic elements.
- Page changes: Repeated lookup can accommodate replaced DOM nodes better than a cached reference, but your test still needs to wait for the relevant transition.
- Locator maintenance: Stable attributes, concise selectors, and page-object methods make changes easier to localize. The right locator depends on the application HTML.
- Cost: @FindBy has no separate service charge; runtime cost comes from the browser session, test infrastructure, and the work the page requires. No performance benchmark is implied here.
8. Capture a page screenshot from Java
If the test also needs a visual artifact, Selenium can save a browser screenshot with its screenshot interface. This is separate from locating elements with @FindBy.
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
public static void saveScreenshot(WebDriver driver, Path destination) throws Exception {
File temporary = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Files.copy(temporary.toPath(), destination);
}
This captures the current browser view exposed by the driver. Ensure the browser is at the intended state before capture, and manage the destination path and temporary-file lifecycle in your test framework.
9. Or skip the browser setup
For a screenshot without running a Selenium browser session, ScreenshotNeo provides a website screenshot API and MCP server. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed, along with known consent platforms, newsletter popups, and chat widgets, before the shot.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status.
- An MCP server lets AI agents use screenshot, page-info, and PDF capture tools.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
10. FAQ
Can I put @FindBy on a class?
The annotation API permits type-level use, but type-level annotations are not processed by default in the usual PageFactory workflow. Put it on the element field for the standard pattern.
Can I use @FindBy on a static field?
The documented pattern is a field on a Page Object instance initialized with that object. Keep elements tied to the driver and page-object instance rather than sharing static fields.
Does PageFactory wait for the element to appear?
Lazy lookup means lookup happens on use; it is not a substitute for waiting for an asynchronous page condition. Apply the synchronization appropriate to your page and test.
Do I need @FindBy on every field?
No. The processor can derive an ID or name locator from an unannotated field name, but explicit annotations make the locator choice clearer when that convention does not fit.


