Resource library

QA How-To

How to Use Playwright locator nth (2026)

Learn playwright locator nth with zero-based indexes, safe use cases, dynamic lists, tables, alternatives, debugging, TypeScript, and interview answers.

21 min read | 2,631 words

TL;DR

Use items.nth(index) to create a Locator for one zero-based position, then await an action or assertion on it. Stabilize and scope the collection first, and prefer identity-based locators whenever reordering should not change the target.

Key Takeaways

  • nth() is zero-based, so index one selects the second match.
  • The result is a lazy Locator, not a pinned element or static snapshot.
  • Use nth() only when position or order is part of the requirement.
  • Scope collections to named regions, table bodies, or component boundaries.
  • Stabilize dynamic lists with loaded-state and retrying count assertions.
  • Prefer first() and last() for boundary positions.
  • Use semantic filtering when business identity matters more than order.

Playwright locator.nth(index) returns a Locator for one zero-based match within an existing locator collection. A correct playwright locator nth test uses position only when order is part of the requirement, such as the second result in a ranked list, the third step in a progress indicator, or a known cell in a stable grid.

The method is simple, but its design tradeoff is important. nth(1) can remove a strictness error while silently targeting the wrong item after sorting, insertion, filtering, responsive rendering, or test-data changes. This guide shows accurate syntax, safe use cases, dynamic-list synchronization, alternatives, debugging, page-object design, and interview answers with runnable TypeScript.

TL;DR

const results = page.getByRole('listitem');

await expect(results).toHaveCount(3);
await expect(results.nth(1)).toContainText('Second result');
Need Preferred locator Why
First item items.first() States intent directly
Last item items.last() Avoids manual count arithmetic
Item at known zero-based index items.nth(index) Position is explicit
Item with business identity items.filter({ hasText: value }) Survives reordering
All stable items for iteration await items.all() after readiness Returns individual locators
Number of items expect(items).toHaveCount(n) Retries for dynamic UI

nth(0) selects the first match, nth(1) the second, and nth(2) the third. The method returns a Locator synchronously. Await the action or assertion performed on it, not locator construction.

1. What playwright locator nth Does

A Playwright Locator describes how to find elements at the moment an operation runs. locator.nth(index) narrows a multi-match locator to the match at the specified zero-based position and returns another Locator.

const items = page.getByRole('listitem');
const secondItem = items.nth(1);

await expect(secondItem).toBeVisible();

The locator remains lazy. If the DOM order changes after secondItem is created but before the assertion, Playwright resolves the current second matching item. It does not pin the original node at index one. This behavior helps with rerenders but makes synchronization and stable order essential.

nth() also suppresses the ambiguity that would otherwise make a single-element action fail. For example, clicking page.getByRole('button', { name: 'Delete' }) fails when several Delete buttons match. Adding nth(1) makes the action target one button, but it does not prove that the second button belongs to the intended record.

That is why position must be a product contract, not a repair tactic. Appropriate examples include verifying the second ranked result, exercising every step in a documented wizard order, or reading a fixed column from a scoped row when semantic column identity is unavailable. Inappropriate examples include choosing the second account because two accounts happen to exist in current fixtures.

Use a semantic filter, named container, label, role, or test ID whenever identity matters more than order. Positional selection is concise, but its correctness depends on the collection and ordering remaining meaningful.

2. Set Up a Runnable nth() Test

Create a Playwright Test project and install browser binaries:

npm init playwright@latest
npx playwright install

Save this test as tests/ranking.spec.ts:

import { test, expect } from '@playwright/test';

test('verifies the second ranked result', async ({ page }) => {
  await page.setContent(`
    <main>
      <h1>Automation tools ranking</h1>
      <ol>
        <li><a href="/tools/alpha">Alpha Runner</a></li>
        <li><a href="/tools/beta">Beta Test</a></li>
        <li><a href="/tools/gamma">Gamma Check</a></li>
      </ol>
    </main>
  `);

  const rankedResults = page.getByRole('listitem');

  await expect(rankedResults).toHaveCount(3);
  await expect(rankedResults.nth(1)).toHaveText('Beta Test');
  await expect(rankedResults.nth(1).getByRole('link'))
    .toHaveAttribute('href', '/tools/beta');
});

Run it with:

npx playwright test tests/ranking.spec.ts

This is a valid positional test because the ordered list and second rank are the behavior under test. The count assertion establishes the expected collection before reading index one. It also makes a missing result fail with a clear list-level message.

In a production suite, replace setContent() with navigation to a controlled page. Stabilize ranking data through fixtures or API setup so the expected order is deterministic. If ranking is calculated dynamically from uncontrolled production-like data, assert ranking rules or visible identity rather than a hard-coded position.

The action or assertion is awaited. const second = rankedResults.nth(1) does not need await because it only creates a Locator.

3. Convert Human Positions to Zero-Based Indexes

Humans say "first," "second," and "third." nth() uses indexes zero, one, and two.

Human position nth() index Clear alternative
First 0 first()
Second 1 nth(1)
Third 2 nth(2)
Last Depends on count last()

Off-by-one errors are the most common positional defect. Give variables human-readable names and keep the conversion visible:

const thirdStepIndex = 2;
const thirdStep = steps.nth(thirdStepIndex);

When input data uses one-based positions, convert once and validate it:

function itemAtPosition(items: Locator, position: number): Locator {
  if (!Number.isInteger(position) || position < 1) {
    throw new Error('position must be a positive integer');
  }
  return items.nth(position - 1);
}

Import Locator as a type when using that helper:

import { type Locator } from '@playwright/test';

Do not calculate the last index with (await items.count()) - 1 when last() states the requirement directly. A count followed by an index can race if the collection changes between operations.

Be explicit about which collection is indexed. page.getByRole('row').nth(1) may select the first data row if the header is index zero. page.locator('tbody').getByRole('row').nth(1) selects the second body row. Correct scope prevents confusing arithmetic and makes reviews easier.

4. Use nth() When Order Is the Requirement

Position is appropriate when a user or specification cares about sequence. Ranked search results, ordered navigation steps, priority queues, visual slots, and keyboard focus sequences can all have meaningful positions.

import { test, expect } from '@playwright/test';

test('shows wizard steps in the required order', async ({ page }) => {
  await page.setContent(`
    <ol aria-label="Application steps">
      <li>Profile</li>
      <li>Experience</li>
      <li>Review</li>
    </ol>
  `);

  const steps = page.getByRole('list', { name: 'Application steps' })
    .getByRole('listitem');

  await expect(steps).toHaveCount(3);
  await expect(steps.nth(0)).toHaveText('Profile');
  await expect(steps.nth(1)).toHaveText('Experience');
  await expect(steps.nth(2)).toHaveText('Review');
});

Here, checking each index is clearer than filtering by text because the test validates order. A text filter could prove every label exists while missing that Experience and Review are swapped.

Another sound use is a repeated set of slots whose labels are absent by design, for example a board with fixed position semantics. Even then, look for accessible names or headings that make the slots clearer.

Document the ordering prerequisite in the test name and setup. If a test clicks the second result, its title should explain why second matters. "Opens the second ranked candidate" is stronger than "opens candidate."

When a collection is user-sortable, set the sort mode and assert it before using nth(). Otherwise, the same index can represent different items across local runs and CI. The Playwright locator assertions guide helps verify the sort label, row count, and expected content before interaction.

5. Avoid nth() When Identity Is Available

The following pattern is fragile:

await page.getByRole('article').nth(2)
  .getByRole('button', { name: 'Open' })
  .click();

It says nothing about which article should open. An inserted promotion, changed sort, or missing fixture record redirects the click while the test may continue.

Filter by identity instead:

const targetArticle = page.getByRole('article').filter({
  has: page.getByRole('heading', { name: 'API Test Strategy' }),
});

await targetArticle.getByRole('button', { name: 'Open' }).click();

Compare selection approaches:

Approach Stable across reorder? Communicates identity? Best use
nth(index) No Only position Ordered behavior
first() or last() No Boundary position First or last contract
filter({ has }) Usually Yes Repeated component by descendant
getByRole(..., { name }) Usually Yes Accessible element identity
getByTestId(id) Usually Technical contract Stable test-specific identity
CSS :nth-child() No DOM sibling position Rare structural assertions

Do not use nth() merely because Playwright reports strictness. Strictness says an action has more than one candidate. Investigate whether the product has duplicate accessible names, the locator is too broad, or data setup created duplicates.

The Playwright locator filter guide shows how to identify a card or row by content instead of position. Use nth only after deciding position is the intended identity.

6. Stabilize Dynamic Lists Before Indexing

A lazy nth locator follows the current order. If a list loads incrementally, resorts after scoring, or inserts a skeleton at the top, index one can point to different nodes across moments.

Wait for a business-ready condition:

import { test, expect } from '@playwright/test';

test('reads the second result after ranking completes', async ({ page }) => {
  await page.setContent(`
    <p role="status">Ranking</p>
    <ol aria-label="Results"></ol>
    <script>
      window.setTimeout(() => {
        document.querySelector("ol").innerHTML =
          "<li>Alpha</li><li>Beta</li><li>Gamma</li>";
        document.querySelector("[role=status]").textContent = "Ranking complete";
      }, 100);
    </script>
  `);

  const results = page.getByRole('list', { name: 'Results' })
    .getByRole('listitem');

  await expect(page.getByRole('status')).toHaveText('Ranking complete');
  await expect(results).toHaveCount(3);
  await expect(results.nth(1)).toHaveText('Beta');
});

There is no waitForTimeout(). The status and count describe observable readiness. If the expected count varies, assert a stable completion signal and the identity at the target position.

Avoid using locator.all() before loading finishes. all() returns the current array of locators immediately. It is useful only after the collection is known to be stable. A retrying assertion against items.nth(index) can wait for that indexed element, but it cannot prove the list will not reorder a moment later. The application's completion signal provides that guarantee.

Virtualized lists add another concern. The DOM may contain only visible rows, so nth(20) means the twenty-first rendered match, not necessarily data record 21. Scroll through the widget using its supported behavior or locate a record by identity. Do not equate DOM index with dataset index unless the component contract guarantees it.

7. Scope nth() for Tables, Cards, and Nested Lists

Index the smallest meaningful collection. Page-wide button or row indexes are hard to interpret because unrelated regions contribute matches.

const invoicesTable = page.getByRole('table', { name: 'Invoices' });
const bodyRows = invoicesTable.locator('tbody').getByRole('row');
const secondInvoice = bodyRows.nth(1);

await expect(secondInvoice).toContainText('INV-1002');

Scoping to tbody prevents the header row from shifting data indexes. Scoping to the named table prevents other tables from contributing rows.

For nested lists, choose the parent first:

const activeProject = page.getByRole('region', { name: 'Active project' });
const tasks = activeProject.getByRole('listitem');
const secondTask = tasks.nth(1);

If each list item contains a nested list, a broad getByRole('listitem') may include descendants at several levels. Scope to the direct list container or use a locator that represents the correct component boundary.

For cards, filter by a category or region before applying position when both classification and order matter:

const featuredCards = page.getByRole('article')
  .filter({ hasText: 'Featured' });

await expect(featuredCards).toHaveCount(3);
await featuredCards.nth(1).getByRole('link').click();

This means "second featured card," not "second card on the page." The requirement should use the same language.

Semantic scope makes positional selection reviewable. The Playwright getByRole guide explains how named regions, tables, and lists improve locator boundaries.

8. Debug playwright locator nth Failures

Start with the collection, not the index. Highlight or assert what contributes to the match:

const items = page.getByRole('listitem');
await expect(items).toHaveCount(3);
await expect(items.nth(1)).toHaveText('Expected second item');

If count is wrong, investigate loading, hidden duplicates, nested list items, responsive variants, or test-data drift. If count is correct but content is wrong, the sort order or index assumption is wrong.

Playwright Inspector can show matching elements:

npx playwright test tests/list.spec.ts --debug

Trace Viewer helps with CI-only order changes. Inspect the DOM snapshot before the action, the selected locator, network timing, and any state transition that inserted or moved items.

Typical symptoms and causes:

  1. Timeout on nth(2): fewer than three elements ever matched.
  2. Wrong record opened: the order changed while the index remained valid.
  3. Local pass, CI failure: uncontrolled data or asynchronous sorting differs.
  4. Header selected as data: the row collection includes thead.
  5. Hidden control clicked: desktop and mobile variants share the collection.
  6. Index changes after scrolling: list virtualization recycles rendered nodes.
  7. Action passes, assertion fails: a valid position targeted the wrong business entity.

Do not fix these by increasing timeouts or changing index values until the test passes. Establish the intended collection, readiness condition, and ordering rule. Then decide whether positional selection is still correct.

9. Choose nth(), first(), last(), or all()

Each collection API expresses a different intention.

first() and last() are clearer for boundaries:

await expect(notifications.first()).toContainText('Newest');
await expect(notifications.last()).toContainText('Oldest');

nth(index) is right for a specific middle position:

await expect(podiumPlaces.nth(1)).toContainText('Second place');

all() returns an array of Locators for stable iteration:

await expect(tags).toHaveCount(3);

for (const tag of await tags.all()) {
  await expect(tag).toBeVisible();
}

Do not use all() on a changing list without establishing stability. When the expected texts are known, a collective locator assertion may be simpler:

await expect(tags).toHaveText(['smoke', 'api', 'ui']);

A collective assertion verifies count, content, and order in one readable line. Use individual nth assertions when each position has different behavior or extra descendants to verify.

A count plus repeated nth calls is acceptable for a fixed ordered component. Avoid manual loops that read count() and then act on a live changing list, since deletion or insertion can shift later indexes during the loop.

10. Use nth() in Page Objects Without Hiding Risk

A page object method can expose explicit positional language:

import { type Locator, type Page } from '@playwright/test';

export class RankingPage {
  readonly results: Locator;

  constructor(page: Page) {
    this.results = page.getByRole('list', { name: 'Ranked candidates' })
      .getByRole('listitem');
  }

  resultAtRank(rank: number): Locator {
    if (!Number.isInteger(rank) || rank < 1) {
      throw new Error('rank must be a positive integer');
    }
    return this.results.nth(rank - 1);
  }
}

The public method accepts a human one-based rank, validates it, and makes the conversion once. Its name states that position is a domain concept. A generic getNthElement(index) helper adds little value and can spread fragile selection throughout the suite.

Keep identity-based methods alongside positional ones:

resultNamed(name: string): Locator {
  return this.results.filter({
    has: this.results.page().getByRole('heading', { name }),
  });
}

A simpler implementation would store the Page and create the inner heading from it, which is often clearer:

resultNamed(name: string): Locator {
  return this.results.filter({
    has: this.page.getByRole('heading', { name }),
  });
}

Declare private readonly page: Page in the constructor for that version. Choose resultAtRank() when rank is the test subject and resultNamed() when candidate identity is.

Do not catch strictness or timeout errors and silently fall back to the first item. A fallback changes test meaning and can make a dangerous action target the wrong record.

11. Review Positional Tests for Long-Term Reliability

Before approving an nth locator, ask:

  1. Is position explicitly named in the requirement or test title?
  2. Is the indexed collection scoped to the correct region?
  3. Are headers, nested items, and hidden variants excluded?
  4. Does the test establish list readiness and expected count?
  5. Is sorting deterministic and asserted?
  6. Could identity-based filtering express the requirement better?
  7. Is the index zero-based and correctly converted from human language?
  8. Can virtualization make rendered position differ from data position?
  9. Does the final assertion prove the correct business item responded?
  10. Will inserted data redirect the action without a clear failure?

If position is correct, make it visible in names such as secondRankedResult or reviewStepIndex. If position is incidental, replace it with role, label, text, a relative filter, or a team-owned test ID.

Be particularly cautious with destructive actions. deleteButtons.nth(1).click() is unacceptable when a user ID or row identity is available. Scope the record first, then click its Delete button. Positional mistakes in read-only assertions are confusing; positional mistakes in mutation workflows can corrupt test data or mask authorization defects.

Use trace retention on retry for CI and deterministic fixtures for ordered data. The goal is not to ban nth(). It is to make every positional locator carry an explicit, reviewable reason.

Interview Questions and Answers

Q: Is Playwright locator nth zero-based?

Yes. nth(0) selects the first match, nth(1) the second, and nth(2) the third. I use first() or last() when those boundary intentions are clearer.

Q: Does nth() return an ElementHandle?

No. It returns another Locator, so resolution remains lazy. The indexed element is determined when an action or assertion runs, which means current order matters.

Q: Why can nth() make a strictness error disappear but still be wrong?

It chooses one positional match from a collection, so the action no longer sees several candidates. However, it does not establish business identity. If order changes, the same index can target a different item while the action still succeeds.

Q: When is nth() a good locator choice?

It is appropriate when position is the behavior, such as ranked results, ordered steps, or a fixed slot. I stabilize the list, assert count or sort state, and name the positional requirement in the test.

Q: How do you synchronize nth() with a dynamic list?

I wait for a business-complete signal and use retrying locator assertions such as toHaveCount(). I avoid fixed sleeps and do not assume all() waits for a changing list.

Q: How do nth() and CSS :nth-child differ?

nth() selects by position within the current Playwright locator match set. CSS :nth-child() selects an element based on its sibling position in DOM structure. Locator nth is usually easier to scope semantically, but both are positional.

Q: How do you test the last item in a list?

I use locator.last() rather than reading count and subtracting one. It states the boundary intent and avoids a snapshot arithmetic race if the list changes.

Common Mistakes

  • Treating nth(1) as the first element instead of the second.
  • Awaiting locator construction conceptually and forgetting to await the actual action or assertion.
  • Adding nth to silence strictness without identifying the intended record.
  • Indexing all rows and accidentally counting the table header.
  • Using page-wide button indexes instead of scoping a component or region.
  • Reading a one-time count before an asynchronous list finishes loading.
  • Assuming a stored nth Locator pins the original DOM element.
  • Using DOM index as a dataset index in a virtualized list.
  • Calculating the last index manually instead of using last().
  • Iterating all() while the underlying list is changing.
  • Clicking a destructive action by position when stable record identity exists.
  • Failing to assert sort mode before testing ranked or ordered content.

A quick test is to reorder the fixture mentally. If the scenario should still target the same business item, nth() is probably the wrong selector. If the expected result should change because second place is the contract, positional selection is appropriate.

Conclusion

Use playwright locator nth for deliberate positional behavior, and remember that its index is zero-based and its result remains a lazy Locator. Scope the collection, prove it is ready, assert its count or sort state, then interact with the intended position.

Do not use nth() as a universal strictness fix. When identity matters, choose a semantic locator or filter the container by stable product data. Review one positional selector in your suite today and either document why order matters or replace it with an identity-based locator.

Interview Questions and Answers

How does locator.nth() work in Playwright?

It returns a Locator for one zero-based position within the current match set. The locator remains lazy, so it resolves current DOM order when an action or assertion executes.

When is nth() an appropriate choice?

I use it when order is explicitly part of the product contract, such as rankings or ordered steps. I scope and stabilize the collection, then assert count or sort state before relying on the index.

Why should nth() not be a default strictness fix?

It removes ambiguity by choosing a position but does not establish business identity. Reordering can redirect the action while the test continues. I prefer semantic scope or filtering when the same record should remain the target.

How do you avoid off-by-one errors with nth()?

I remember that nth is zero-based, use first() for the first item, and name indexes in domain terms. When accepting a human rank, I validate it and convert rank minus one in a single helper.

How do you synchronize nth() with dynamic content?

I wait for a business-ready signal and use retrying locator assertions such as toHaveCount(). I avoid fixed sleeps, early all() calls, and one-time counts before ranking or loading completes.

How do nth(), first(), and last() compare?

first() and last() make boundary intent explicit. nth(index) is for a known middle or general position. All return Locators and remain lazy.

What special issue occurs with virtualized lists?

The DOM may contain only currently rendered rows, so locator position may not equal dataset position. I use record identity or the widget's supported scroll behavior instead of assuming nth maps to a global data index.

Frequently Asked Questions

Is Playwright nth zero-based?

Yes. nth(0) selects the first match, nth(1) selects the second, and nth(2) selects the third. Use first() when first position is the clearest intent.

Does locator.nth() need await?

No. nth() synchronously returns another Locator. Await the action or assertion performed on that locator, such as click() or expect(locator).toBeVisible().

Does nth() wait for an element?

The returned Locator is resolved during an action or assertion, which can auto-wait according to that operation. Stabilize dynamic list order with a loaded-state signal and retrying assertions before relying on position.

How do I select the last element in Playwright?

Use locator.last(). It communicates the boundary requirement directly and avoids reading a snapshot count and subtracting one.

Why is Playwright nth considered fragile?

Position can change when data, sorting, responsive variants, or DOM structure changes. The same index may still exist and target the wrong business item without a strictness error.

How do I use nth with a table row?

Scope to the named table and usually to tbody before applying nth, so header rows do not shift the index. Assert loaded count and expected row content before a consequential action.

Should I use nth to fix strict mode violations?

Only if position is genuinely the requirement. Otherwise, strengthen identity with a role, label, exact text, test ID, or filter based on a descendant.

Related Guides