jQuery / Selector / :odd - Select Elements at Odd Indexes
jQuery's :odd selects indexes 1, 3, 5 and so on in the matched set, counting from zero. These are the second, fourth and sixth elements, so expecting the first visible row from the word “odd” leads to the opposite result. For new code, select the scope with an ordinary CSS selector and then filter with .odd().
Meaning and migration to .odd()
The :odd documentation marks this selector deprecated since jQuery 3.4 and recommends the .odd() method. Added in 3.5, .odd() takes no arguments and creates a new jQuery object containing the odd-indexed elements of the current set. Deprecation is different from removal: the comparison in this article was executed successfully with both 3.7.1 and 4.0.0.
Replace $("#records > tbody > tr:odd") with $("#records > tbody > tr").odd(). Versions older than 3.5 do not have this method, so check application and plugin compatibility before upgrading.
| Expression | Counting rule | Initial result |
|---|---|---|
| :odd or .odd() on tbody rows | The selected data-row set, from zero | r2, r4 |
| .odd() on all rows including the header | The header occupies index zero | r1, r3, r5 |
| tbody tr:nth-child(odd) | Sibling positions within each parent, from one | r1, r3, r5 |
Run a complete table example
Save all the following code as UTF-8 in odd-demo.html and open it in a browser. No package installation is required. An internet connection is needed to load the official distribution from its Content Delivery Network (CDN). The example uses 4.0.0.
The thead contains one header row and tbody contains five data rows. Row IDs identify items; indexes come from the current document order and selection scope. Use the buttons to change those conditions.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Odd indexes in table rows</title>
<style>
body { font: 16px/1.6 sans-serif; margin: 24px; }
table { border-collapse: collapse; width: 100%; max-width: 600px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
tr.marked { background: #fff0b3; }
button { margin: 4px; padding: 8px; }
</style>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
</head>
<body>
<h1>Odd indexes in table rows</h1>
<table id="records">
<thead><tr id="heading"><th>Item</th><th>Amount</th></tr></thead>
<tbody>
<tr id="r1" class="eligible"><td>Item 1</td><td>10</td></tr>
<tr id="r2"><td>Item 2</td><td>20</td></tr>
<tr id="r3" class="eligible"><td>Item 3</td><td>30</td></tr>
<tr id="r4" class="eligible"><td>Item 4</td><td>40</td></tr>
<tr id="r5" class="eligible"><td>Item 5</td><td>50</td></tr>
</tbody>
</table>
<button type="button" id="data">Data rows .odd()</button>
<button type="button" id="all">Include header .odd()</button>
<button type="button" id="css">Data :nth-child(odd)</button>
<button type="button" id="prepend">Prepend one data row</button>
<button type="button" id="reset">Reset</button>
<p id="result" aria-live="polite"></p>
<script>
$(function () {
function show($chosen) {
$("#records tr").removeClass("marked");
$chosen.addClass("marked");
const ids = $chosen.map((_, row) => row.id).get().join(", ");
$("#result").text(`${ids || "none"} | ${$chosen.length}`);
}
function dataRows() {
show($("#records > tbody > tr").odd());
}
$("#data").on("click", dataRows);
$("#all").on("click", () => show($("#records tr").odd()));
$("#css").on("click", () => show($("#records > tbody > tr:nth-child(odd)")));
$("#prepend").on("click", function () {
if (!$("#extra").length) {
$("#records > tbody").prepend('<tr id="extra"><td>Extra item</td><td>0</td></tr>');
}
dataRows();
});
$("#reset").on("click", function () {
$("#extra").remove();
dataRows();
});
dataRows();
});
</script>
</body>
</html>
Initially, only data rows are selected before applying .odd(). Item 2 and Item 4 are highlighted, with this result:
r2, r4 | 2
Change scope, insert a row and reset
- Data rows .odd() counts only tr elements directly inside tbody. Indexes 1 and 3 produce
r2, r4 | 2. - Include header .odd() includes the thead row. The header takes index zero, shifting the result to
r1, r3, r5 | 3. - Data :nth-child(odd) selects sibling positions 1, 3 and 5 inside tbody:
r1, r3, r5 | 3. Sharing the previous button's result does not mean sharing its counting rule. - Prepend one data row inserts Extra item at the start of tbody and recomputes .odd(). The former r1 moves to index 1, giving
r1, r3, r5 | 3. Checking the extra ID prevents repeated clicks from inserting duplicates. - Reset removes only the added row and reapplies .odd() to the data rows. The original five rows and values remain, restoring
r2, r4 | 2.
show removes all previous marked classes before marking the new selection, so highlights do not accumulate. In a real table, constrain the ID and direct-child relationships to avoid including unrelated or nested tables.
Compare with CSS odd
Immediately after opening or pressing Reset, run this fragment in the developer console. The first two lines compare the legacy selector and replacement; the last uses :nth-child(), which follows CSS rules.
const ids = $set => $set.map((_, row) => row.id).get().join(", ");
console.log(ids($("#records > tbody > tr:odd")));
console.log(ids($("#records > tbody > tr").odd()));
console.log(ids($("#records > tbody > tr:nth-child(odd)")));
r2, r4
r2, r4
r1, r3, r5
.odd() counts one combined matched set, whereas :nth-child(odd) restarts sibling positions for each parent, including separate tbody elements. :nth-child() counts all element siblings regardless of tag, then requires the candidate at that position to match tr. Whitespace text and comments are not element siblings.
Filter order, empty sets and dynamic rows
Rows r1, r3, r4 and r5 have the eligible class. $("#records > tbody > tr.eligible").odd() collects those four rows first and keeps r3 and r5. Applying .odd() to all five data rows before .filter(".eligible") keeps only r4 from r2 and r4. Filter order changes the set whose indexes are calculated.
An empty or single-element set has no index 1, so .odd() returns an empty set. Results do not update automatically: a selection storing r2 and r4 before insertion still contains those elements afterward. Query again, as dataRows does, to reflect current positions.
If nothing happens, check the jQuery request in Network, errors in Console and jQuery.fn.jquery. For an inverted result, inspect header inclusion, tbody scope, filter order and any inserted row. Keep visible row numbers separate from zero-based indexes.




