Core Tools intermediate position indexing

Position and Indexing

position(), last(), numeric predicates, the difference between //li[1] and (//li)[1], and why ancestor::div[1] is the nearest ancestor rather than the outermost.

Position and Indexing

Indexes are the most fragile thing you can put in a locator, and also unavoidable in some places: “the third row”, “the last notification”, “the second Save button”. This lesson makes sure that when you do use one, it selects what you think it does.

Positions are 1-based

XPath counts from 1. [1] is the first node, [0] is nothing.

//li[1]        first li child of each parent
//li[2]        second li child of each parent
//li[last()]   last li child of each parent
//li[last()-1] second-to-last

last() returns the size of the current context, so [last()] is always the final node, however many there are.

Per-parent versus global

A numeric predicate applies within the step it is attached to. For //li[1] the step is “li children of some parent”, so you get the first li of every parent.

<ul id="a"><li>A1</li><li>A2</li></ul>
<ul id="b"><li>B1</li><li>B2</li></ul>
//li[1]        two results: A1 and B1
(//li)[1]      one result: A1
(//li)[last()] one result: B2
//ul[2]/li[1]  one result: B1

Parentheses turn the whole node-set into a single context, and then the index applies globally in document order. Reach for (...)[n] whenever you mean “the nth match on the page”.

position() with comparisons

[n] is shorthand for [position()=n]. The long form lets you use ranges:

//tr[position() > 1]                    all rows after the header row
//tr[position() <= 5]                   first five rows
//tr[position() >= 2 and position() <= 4]   rows 2 to 4
//li[position() mod 2 = 0]              even items
//li[position() != last()]              everything except the last

position() is evaluated after the preceding predicates, so order matters:

//tr[@data-status='open'][1]     first open row
//tr[1][@data-status='open']     first row, only if it is open

Reverse axes count backwards

This surprises almost everyone. On a reverse axis (ancestor, ancestor-or-self, preceding, preceding-sibling), position 1 is the node nearest the context node.

<div id="outer">
  <div id="middle">
    <div id="inner">
      <button>Go</button>
    </div>
  </div>
</div>
//button/ancestor::div[1]                    #inner (nearest)
//button/ancestor::div[last()]               #outer (furthest)
//button/preceding-sibling::*[1]             the sibling immediately before the button
(//button/ancestor::div)[1]                  #outer, because parentheses reset to document order

So preceding-sibling::td[1] is “the cell just to the left”, which is usually what you want, and ancestor::form[1] is “the closest form”.

Anchoring an index safely

If you need an index, keep it inside a stable container so that unrelated page changes cannot shift it:

Fragile: (//button)[7] counts every button on the page. A new cookie banner breaks it.

Better: //div[@data-testid=‘toolbar’]//button[3] counts only inside the toolbar.

Best: //div[@data-testid=‘toolbar’]//button[@aria-label=‘Bold’] does not count at all.

Indexes in tables

Column indexes are the one place where a number is often the right choice, because the column position is part of the table’s meaning:

//tr[td[1][normalize-space()='Widget']]/td[3]        price cell for the Widget row
//tr[normalize-space(td[1])='Widget']/td[last()]     last cell in that row

Even so, computing the column from its header survives column reordering. See Working with Tables.

Selecting by count

You can filter a container by how many children it has:

//ul[count(li) = 0]         empty lists
//ul[count(li) > 10]        long lists
//tr[count(td) != count(//tr[1]/th)]   rows whose cell count differs from the header

Try It Yourself

Open in Playground →

Then try //tbody/tr/td[2][1] and (//tbody/tr/td[2])[1] and note the difference in the match count.

Next Steps

  1. Logic: and, or, not - Replacing indexes with conditions
  2. XPath Axes Navigation - The reverse axes in context
  3. Lists and Repeated Components - Picking one card out of many without counting