Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use a positional CSS selector scoped to the table you mean: table tbody tr:nth-of-type(2). In Puppeteer, read that row with $eval, or use $$eval and take index 1 when a second row might be absent. Scoping to tbody prevents a header row from being counted as data.

The quickest working examples

Read the second data row with $eval

const secondRowText = await page.$eval(
  'table tbody tr:nth-of-type(2)',
  row => row.textContent,
);

console.log(secondRowText);

$eval runs the callback on the first element matching the selector. If the selector matches no row, Puppeteer throws an error, so use this form when two rows are required for the test or workflow.

Read it safely with $$eval

const secondRowText = await page.$$eval(
  'table tbody tr',
  rows => rows[1]?.textContent ?? null,
);

if (secondRowText === null) {
  console.log('The table has fewer than two data rows');
}

$$eval collects every matching row and passes the array to your callback. JavaScript index 1 is the second item because arrays start at zero. Optional chaining returns null instead of failing when the second row does not exist.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the selector that matches your table markup

tbody tr:nth-of-type(2): usually the right data-row selector

'#results tbody tr:nth-of-type(2)'

nth-of-type(2) selects the second tr sibling inside the selected parent. Adding tbody means the count starts with body rows, not a header row in thead. Add a stable table ID, class, or container when the page contains more than one table.

#1 Best Overall
Sale
Philips 24 Inch Computer Monitor FHD 100Hz VA VESA Flicker-Free, 241V8LB
  • CRISP CLARITY: This 23.8″ Philips V line monitor delivers crisp Full HD 1920x1080 visuals. Enjoy movies, shows and videos with remarkable detail
  • INCREDIBLE CONTRAST: The VA panel produces brighter whites and deeper blacks. You get true-to-life images and more gradients with 16.7 million colors
  • THE PERFECT VIEW: The 178/178 degree extra wide viewing angle prevents the shifting of colors when viewed from an offset angle, so you always get consistent colors
  • WORK SEAMLESSLY: This sleek monitor is virtually bezel-free on three sides, so the screen looks even bigger for the viewer. This minimalistic design also allows for seamless multi-monitor setups that enhance your workflow and boost productivity
  • A BETTER READING EXPERIENCE: For busy office workers, EasyRead mode provides a more paper-like experience for when viewing lengthy documents

nth-child(2): a different counting rule

'#results tbody tr:nth-child(2)'

nth-child(2) means “the second element child of this parent, and that child must be a tr.” Other element children can change the result. By contrast, nth-of-type(2) counts only siblings of the same element type. Inspect the actual DOM before choosing between them.

table tr:nth-of-type(2): count rows across table sections

'#results tr:nth-of-type(2)'

This can be appropriate when the second row should be counted among all matching rows in the table, but it may include a header row depending on the markup. If “second row” means second data row, prefer tbody.

Scope the query when several tables exist

Positional selectors are meaningful only within the elements they match. A page-wide selector such as table tbody tr:nth-of-type(2) can match the second row in the first table encountered, which may not be the table your test needs. Use a stable ancestor:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const row = await page.$eval(
  '#orders tbody tr:nth-of-type(2)',
  element => ({
    text: element.textContent.trim(),
    cells: [...element.querySelectorAll('td')].map(cell => cell.textContent.trim()),
  }),
);

Prefer IDs or semantic container attributes that are part of the page’s contract. Avoid selectors based on generated class names that change between builds.

Rank #2
Philips 22 Inch Computer Monitor FHD 100Hz VA VESA Flicker-Free, 221V8LB
  • CRISP CLARITY: This 22 inch class (21.5″ viewable) Philips V line monitor delivers crisp Full HD 1920x1080 visuals. Enjoy movies, shows and videos with remarkable detail
  • 100HZ FAST REFRESH RATE: 100Hz brings your favorite movies and video games to life. Stream, binge, and play effortlessly
  • SMOOTH ACTION WITH ADAPTIVE-SYNC: Adaptive-Sync technology ensures fluid action sequences and rapid response time. Every frame will be rendered smoothly with crystal clarity and without stutter
  • INCREDIBLE CONTRAST: The VA panel produces brighter whites and deeper blacks. You get true-to-life images and more gradients with 16.7 million colors
  • THE PERFECT VIEW: The 178/178 degree extra wide viewing angle prevents the shifting of colors when viewed from an offset angle, so you always get consistent colors

Extract cells and attributes from the second row

Return an array of cell text

const cells = await page.$eval(
  '#orders tbody tr:nth-of-type(2)',
  row => [...row.querySelectorAll('th, td')]
    .map(cell => cell.textContent.trim()),
);

Read a link or data attribute

const detailsUrl = await page.$eval(
  '#orders tbody tr:nth-of-type(2) a.details',
  link => link.href,
);

const orderId = await page.$eval(
  '#orders tbody tr:nth-of-type(2)',
  row => row.getAttribute('data-order-id'),
);

These nested selectors still depend on the second row being present. Use $$eval when an empty table is a valid state.

Select the second row for an action

For clicking, typing, or another interaction, Puppeteer’s Locator API is the recommended way to select an element and interact with it. A locator can wait for the element and relevant action conditions instead of requiring a manual existence check.

const secondRow = page.locator('#orders tbody tr:nth-of-type(2)');
await secondRow.locator('button.details').click();

Use a row locator followed by a control selector so the button is resolved within the intended row. If the row is rendered asynchronously, wait for the table or row condition your page requires before interacting.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for dynamic table content

A selector cannot find rows that have not been inserted yet. Navigate first, then wait for a stable condition:

Rank #3
Sale
Dell 24 Monitor - SE2426H - 23.8-inch FHD (1920x1080) 144Hz 1ms Display, in-Plane Switching (IPS) Technology, AMD FreeSync™, TÜV 3-Star 2X HDMI, Tilt
  • Clear visuals. Fluid motion: A 144Hz refresh rate and 1ms MPRT deliver smooth, tear‑free motion across work, gaming, and streaming for clearer, more fluid viewing.
  • Eye comfort: TÜV Rheinland 3‑star* certification reduces harmful blue light while preserving stunning color quality without compromise. *TÜV Rheinland 3-star eye comfort certification.
  • Wide viewing angle: Get consistent views across a wide 178° /178° viewing angle.
  • In-Plane Switching (IPS): See excellent color accuracy and consistency across wide viewing angles with In-plane Switching (IPS) technology.
  • Ultra-thin bezels: Maximize your viewing experience with thin bezels.
await page.goto('https://example.com/orders', { waitUntil: 'networkidle0' });
await page.waitForSelector('#orders tbody tr');

const second = await page.$$eval(
  '#orders tbody tr',
  rows => rows[1]?.textContent.trim() ?? null,
);

If the application adds rows after an API response, a first-row wait may still finish before the second row arrives. In that case, wait for a selector that represents the second row, or use a locator and an assertion that reflects your test framework. Do not add an arbitrary delay unless the site gives you no more reliable condition.

Handle missing rows deliberately

Missing rows are an error

Use $eval when the absence of a second row should fail the operation:

const text = await page.$eval(
  '#results tbody tr:nth-of-type(2)',
  row => row.textContent.trim(),
);

The thrown error identifies a broken assumption, such as an empty result set, a changed selector, or content that has not loaded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Missing rows are expected

const second = await page.$$eval(
  '#results tbody tr',
  rows => rows.length > 1 ? rows[1].textContent.trim() : null,
);

This explicitly distinguishes “a second row is absent” from a Puppeteer failure. You can then branch to an empty-state message, skip a test, or return a structured result.

Rank #4
Samsung 27" Essential S3 (S36GD) Series FHD 1800R Curved Computer Monitor
  • CURVED FOR ENHANCED ENGAGEMENT: An immersive viewing experience with a curved monitor that wraps more closely around your field of vision; It creates a wider view, enhancing depth perception and minimizing peripheral distraction
  • SMOOTH PERFORMANCE FOR SEAMLESS CONTENT: Stay in the action when playing games, watching videos, or working on creative projects; The 100Hz refresh rate reduces lag and motion blur so you don't miss a thing in fast-paced moments¹
  • MORE GAMING POWER: Gain the edge with optimizable game settings; Color and image contrast can be adjusted to see scenes more vividly and spot enemies hiding in the dark; Game Mode adjusts any game to fill the screen so you can view every detail²
  • KEEP IT EASY ON THE EYES: Care for your eyes and stay comfortable, even during long sessions; Advanced eye comfort technology certified by TÜV reduces eye strain by minimizing blue light and reducing irritating screen flicker²
  • INCREASED VERSATILITY: Connect to more; Plug devices straight into your monitor for increased flexibility, making your computing environment even more convenient

Shadow DOM and nested tables

Ordinary CSS selectors do not cross a Shadow DOM boundary. If the table is inside an open component shadow root, the selector must address that boundary using Puppeteer’s supported shadow-root selector syntax or by evaluating within the relevant shadow root. Closed shadow roots cannot be queried from page JavaScript in the normal way.

Nested tables create a similar ambiguity: table tbody tr:nth-of-type(2) can match a row in an inner table if the selector is not scoped. Anchor the query to the outer table’s stable ID or container and inspect the DOM hierarchy.

Common failures and fixes

Symptom Likely cause Fix
The selected row is the header The selector counts all table rows. Use tbody tr:nth-of-type(2) when you mean the second data row.
No element matches There are fewer than two rows, the table is still loading, or the selector is wrong. Wait for the correct condition, inspect the DOM, and use $$eval with a length check when absence is valid.
The wrong table is selected The page contains multiple tables. Scope the selector with a stable ID, class, or ancestor.
nth-child(2) returns nothing The second element child is not a tr. Use nth-of-type(2) or adjust the selector to the actual sibling structure.
Click races with rendering The row or button is inserted after navigation. Use a Locator and wait for the application’s ready state.
A component table cannot be found The table is inside a shadow root. Use Puppeteer’s shadow-DOM selector support for an open root, or query from the component context.

Verification and maintainability checklist

  • Confirm whether “second row” includes a header or means the second data row.
  • Inspect whether rows are under thead, tbody, tfoot, or a nested table.
  • Scope to one stable table before applying a positional selector.
  • Use nth-of-type when counting tr siblings; use nth-child only when child position is intentional.
  • Choose $eval for a required match and $$eval for explicit missing-row handling.
  • Use Locators for interactions that need automatic waiting.
  • Check the Puppeteer version installed in your project. The documentation pages consulted display 25.12.0 for page interactions, $eval, and Locator references, while the $$eval page displays 25.9.0; APIs should be checked against your installed version when version-specific behavior matters.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your real goal is a clean image or PDF of the page containing the table rather than DOM-level automation, ScreenshotNeo provides a website screenshot API. It accepts the consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server also exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

One-call capture with cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));

See the ScreenshotNeo documentation for the full option set, including full-page lazy-image loading, CSS-selector element capture, device presets, retina scale, PDF paper and page-range controls, custom CSS or JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

Every plan includes all features. The free plan provides 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

Best Value
Sale
Sceptre New 22-Inch Gaming Monitor, FHD 1080p, Up to 144Hz, HDMI, DisplayPort, Built-in Speakers, Machine Black (E225W-FW144 Series, 2026)
  • 【INTEGRATED SPEAKERS】Whether you're at work or in the midst of an intense gaming session, our built-in speakers provide rich and seamless audio, all while keeping your desk clutter-free.
  • 【EASY ON THE EYES】 Protect your eyes and enhance your comfort with Blue-Light Shift technology. This feature reduces harmful blue light emissions from your screen, helping to alleviate eye strain during long hours of use and promoting healthier viewing habits.
  • 【WIDEN YOUR PERSPECTIVE】Our sleek minimal bezel design ensures undivided attention. The nearly bezel-free display seamlessly connects in a dual monitor arrangement, delivering an unobstructed view that lets you focus on more at once, completely distraction-free.

FAQ

What is the difference between the second row and the second data row?

The second row may include a header depending on the markup. The second data row is normally the second tr inside tbody.

Can I select by visible row number?

CSS positional selectors count DOM siblings, not the number displayed by a table’s UI. For paginated or virtualized tables, identify whether the page has rendered only the visible subset before selecting index two.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I use XPath instead?

CSS is sufficient for this case and is accepted by Puppeteer’s selector APIs. XPath is useful only when the relationship you need is not expressed clearly by the available CSS structure.

Frequently Asked Questions

Does $eval wait for the second row automatically?

No. It evaluates immediately against the current DOM. Wait for a reliable row or application condition first, or use a Locator for an interaction that benefits from automatic waiting.

How do I select the second row after sorting?

Apply the selector after sorting has completed and the DOM reflects the new order. Wait for a post-sort signal rather than assuming a fixed delay.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.