Logic: and, or, not
Combine conditions inside predicates, negate correctly with not(), and express 'has none of' without falling into the node-set != trap.
Logic: and, or, not
Real locators usually need more than one condition. XPath gives you and, or and not(), plus a few idioms that replace the operators other languages have.
and
Both conditions must be true for the same context node:
//input[@type='checkbox' and @checked]
//button[@type='submit' and not(@disabled)]
//a[contains(@href, '/orders/') and contains(@class, 'active')]
Chained predicates are equivalent for non-numeric conditions and some people find them easier to read:
//input[@type='checkbox'][@checked]
or
Either condition is enough:
//button[@type='submit' or @type='button']
//*[@data-testid='save' or @data-test='save' or @data-cy='save']
//input[@type='text' or @type='email' or @type='search']
The union operator | is different: it joins two separate expressions rather than two conditions on one node. //button | //a selects buttons and links. //*[self::button or self::a] does the same thing with a predicate. Use whichever reads better.
not()
not() is a function and needs parentheses around its argument:
//button[not(@disabled)] no disabled attribute
//li[not(contains(@class, 'hidden'))] class does not contain hidden
//div[not(.//input)] divs that contain no inputs anywhere
//tr[not(td)] rows with no td cells (header rows)
//input[not(@type) or @type='text'] inputs whose type is missing or text
not(@disabled) tests for the absence of the attribute. It is not the same as @disabled='false'. HTML boolean attributes are true when present with any value, so disabled="false" is still disabled in the browser, and XPath will agree with the browser here only if you test presence.
”none of them” versus “some of them are not”
This is the trap. When the left side of a comparison is a node-set, != is true if any node differs:
<ul>
<li class="done">A</li>
<li class="todo">B</li>
</ul>
//ul[li/@class != 'done'] true, because B's class is not done
//ul[not(li/@class = 'done')] false, because at least one li IS done
So:
- “has at least one li that is not done”:
li/@class != 'done' - “has no li that is done”:
not(li/@class = 'done') - “every li is done”:
not(li[@class != 'done']), meaning there is no li whose class differs
There is no every or some keyword in XPath 1.0. You build them from not() and existence.
Existence as a condition
A path inside a predicate is true if it selects at least one node. This is how you say “has a child like X” or “contains X somewhere”:
//tr[td[@class='error']] rows with an error cell
//form[.//input[@type='password']] forms with a password field somewhere inside
//div[@class='card'][h2] cards that have an h2 child
//li[a] list items that contain a link
//button[svg] buttons containing an inline svg (see the SVG lesson for the namespace catch)
The leading . in .//input is required inside a predicate. Without it, //input restarts from the document root and would be true for every form on the page as long as any password input exists anywhere.
Boolean conversion rules
When a value is used where a boolean is expected, XPath converts it:
| Value | true when |
|---|---|
| node-set | non-empty |
| string | non-empty |
| number | not zero and not NaN |
That is why [@disabled] works as an existence test and why [string-length(@value)] means “value is not empty”. Prefer explicit forms like string-length(@value) > 0 when readability matters.
Precedence
and binds tighter than or:
@a='1' or @b='2' and @c='3' means @a='1' or (@b='2' and @c='3')
Add parentheses whenever both appear in one predicate. Future readers will thank you.
Try It Yourself
Open in Playground →
Next Steps
- Text Matching Functions - Conditions on visible text
- Dynamic Attributes - Conditions that survive generated ids and classes
- Common Errors and Fixes - The error messages you get when logic goes wrong