Understanding Complex Selectors: The Striped Table Example
What is a "complex selector"?
So far you've learned the basics:
#name→ one element by ID.name→ elements by classtag→ elements by tag type[attribute="value"]→ elements by attribute
Complex selectors combine these ideas, and add something new: positional selectors — ways of targeting elements based on where they appear in the page.
Think of it like this. Instead of saying:
"Find all table rows"
You can say:
"Find every second table row" "Find only the first table row" "Find all even-numbered table rows"
That's the power we're about to unlock.
The scenario: A data table
Imagine you have this table in your HTML:
<table>
<tr>
<td>Name</td>
<td>Age</td>
<td>City</td>
</tr>
<tr>
<td>Alice</td>
<td>28</td>
<td>London</td>
</tr>
<tr>
<td>Bob</td>
<td>34</td>
<td>Paris</td>
</tr>
<tr>
<td>Carol</td>
<td>22</td>
<td>Tokyo</td>
</tr>
<tr>
<td>David</td>
<td>41</td>
<td>Berlin</td>
</tr>
</table>Right now, every row looks the same. Plain. No styling.
┌──────────┬─────┬────────┐
│ Name │ Age │ City │ ← Row 1 (header)
├──────────┼─────┼────────┤
│ Alice │ 28 │ London │ ← Row 2
├──────────┼─────┼────────┤
│ Bob │ 34 │ Paris │ ← Row 3
├──────────┼─────┼────────┤
│ Carol │ 22 │ Tokyo │ ← Row 4
├──────────┼─────┼────────┤
│ David │ 41 │ Berlin │ ← Row 5
└──────────┴─────┴────────┘Hard to read. All rows blend together.
What we want is this:
┌──────────┬─────┬────────┐
│ Name │ Age │ City │ ← Dark background, white bold text (header)
├──────────┼─────┼────────┤
│ Alice │ 28 │ London │ ← White background
├──────────┼─────┼────────┤
│ Bob │ 34 │ Paris │ ← Gray background
├──────────┼─────┼────────┤
│ Carol │ 22 │ Tokyo │ ← White background
├──────────┼─────┼────────┤
│ David │ 41 │ Berlin │ ← Gray background
└──────────┴─────┴────────┘Alternating white and gray rows, with a special dark header. Much easier to scan.
Let's break down how the code achieves this.
The full code
Selector.update({
'table tr:nth-child(even)': {
style: { backgroundColor: '#f9fafb' }
},
'table tr:nth-child(odd)': {
style: { backgroundColor: 'white' }
},
'table tr:first-child': {
style: {
backgroundColor: '#1f2937',
color: 'white',
fontWeight: 'bold'
}
}
});Three selectors. Let's learn each one.
Selector 1: table tr:nth-child(even)
'table tr:nth-child(even)': {
style: { backgroundColor: '#f9fafb' }
}Breaking it apart word by word
table tr :nth-child(even)
│ │ │
│ │ └── "whose position number is even"
│ └──────────── "find tr elements (table rows)..."
└─────────────────── "inside a table..."What is :nth-child?
:nth-child is a positional selector. It looks at where an element sits in relation to its siblings — elements that share the same parent.
Think of it like numbering seats in a cinema row:
Seat 1 Seat 2 Seat 3 Seat 4 Seat 5
(odd) (even) (odd) (even) (odd):nth-child(even) picks seats 2, 4, 6, 8... all the even-numbered ones.:nth-child(odd) picks seats 1, 3, 5, 7... all the odd-numbered ones.
Applied to our table
Row 1: tr (position 1 = odd) → NOT targeted by :nth-child(even)
Row 2: tr (position 2 = even) → ✅ TARGETED — gets gray background
Row 3: tr (position 3 = odd) → NOT targeted
Row 4: tr (position 4 = even) → ✅ TARGETED — gets gray background
Row 5: tr (position 5 = odd) → NOT targetedWhat does #f9fafb mean?
That's a hex color code — a way of writing colors in code.
#f9fafb is a very light gray. Almost white, but not quite.
You can think of hex colors as a secret recipe:
#ffffff= pure white#f9fafb= barely-there gray (our even rows)#9ca3af= medium gray#000000= pure black
Selector 2: table tr:nth-child(odd)
'table tr:nth-child(odd)': {
style: { backgroundColor: 'white' }
}This is the mirror image of the previous selector. It targets rows 1, 3, 5 — the odd-numbered ones — and sets their background to white.
Row 1: tr (position 1 = odd) → ✅ TARGETED — gets white background
Row 2: tr (position 2 = even) → NOT targeted (already gray)
Row 3: tr (position 3 = odd) → ✅ TARGETED — gets white background
Row 4: tr (position 4 = even) → NOT targeted (already gray)
Row 5: tr (position 5 = odd) → ✅ TARGETED — gets white backgroundWait — why target odd rows if white is the default?
Good question! You might think: "white is already the default, so why bother?"
Because you might be adding this to a page that already has some other color applied. Being explicit means your styling is predictable and reliable, regardless of what was there before.
After these two selectors run, the table looks like this:
┌──────────┬─────┬────────┐
│ Name │ Age │ City │ ← Row 1 (white)
├──────────┼─────┼────────┤
│ Alice │ 28 │ London │ ← Row 2 (gray)
├──────────┼─────┼────────┤
│ Bob │ 34 │ Paris │ ← Row 3 (white)
├──────────┼─────┼────────┤
│ Carol │ 22 │ Tokyo │ ← Row 4 (gray)
├──────────┼─────┼────────┤
│ David │ 41 │ Berlin │ ← Row 5 (white)
└──────────┴─────┴────────┘Getting better! But the header row still looks like regular data. Let's fix that.
Selector 3: table tr:first-child
'table tr:first-child': {
style: {
backgroundColor: '#1f2937',
color: 'white',
fontWeight: 'bold'
}
}Breaking it apart
table tr :first-child
│ │ │
│ │ └── "that is the first one"
│ └──────────── "find tr elements (rows)..."
└─────────────────── "inside a table..."What is :first-child?
:first-child targets the very first element in a group. No counting needed — just "give me the first one."
In our table, that's Row 1 — the header row with "Name", "Age", "City".
Three style properties at once
style: {
backgroundColor: '#1f2937', // Very dark navy/charcoal color
color: 'white', // Text color becomes white
fontWeight: 'bold' // Text becomes bold/thick
}backgroundColor: '#1f2937'
A very dark blue-gray color. Professional and striking.
color: 'white'
This changes the text color to white (not the background — that's backgroundColor). White text on a dark background creates strong contrast.
fontWeight: 'bold'
Makes the text thicker and heavier. bold is the value, similar to pressing Ctrl+B in a word processor.
After this selector runs:
┌──────────┬─────┬────────┐
│ Name │ Age │ City │ ← Dark bg, white bold text ✅
├──────────┼─────┼────────┤
│ Alice │ 28 │ London │ ← White (from odd)
├──────────┼─────┼────────┤
│ Bob │ 34 │ Paris │ ← Gray (from even)
├──────────┼─────┼────────┤
│ Carol │ 22 │ Tokyo │ ← White (from odd)
├──────────┼─────┼────────┤
│ David │ 41 │ Berlin │ ← Gray (from even)
└──────────┴─────┴────────┘The table is now styled, striped, and readable.
The order matters!
Notice that the header row (Row 1) is odd-numbered. So the odd selector also gives it a white background. But then — the first-child selector runs after and overwrites it with the dark color.
Think of it like painting:
- Paint all odd rows white
- Paint all even rows gray
- Paint the first row dark (overwrites the white from step 1)
The last style applied wins. That's why the header ends up dark, even though it's technically an "odd" row.
Why is this better than doing it manually?
The old-fashioned way of achieving this would be:
Option A: Add classes to every row manually in HTML
<table>
<tr class="header-row">...</tr>
<tr class="even-row">...</tr>
<tr class="odd-row">...</tr>
<tr class="even-row">...</tr>
<tr class="odd-row">...</tr>
</table>This is tedious. And what if your table has 500 rows? Or the data is generated dynamically?
Option B: Loop through rows in JavaScript
const rows = document.querySelectorAll('table tr');
rows.forEach((row, index) => {
if (index === 0) {
row.style.backgroundColor = '#1f2937';
row.style.color = 'white';
row.style.fontWeight = 'bold';
} else if (index % 2 === 0) {
row.style.backgroundColor = '#f9fafb';
} else {
row.style.backgroundColor = 'white';
}
});This works but it's hard to read at a glance, and it mixes position logic with styling.
Option C: Selector.update() (what we just learned)
Selector.update({
'table tr:nth-child(even)': { style: { backgroundColor: '#f9fafb' } },
'table tr:nth-child(odd)': { style: { backgroundColor: 'white' } },
'table tr:first-child': { style: { backgroundColor: '#1f2937', color: 'white', fontWeight: 'bold' } }
});Each selector describes what it does by its very name. You read it and instantly know what's being targeted.
The cheat sheet for positional selectors
:first-child → The first element in its group
:last-child → The last element in its group
:nth-child(2) → The second element exactly
:nth-child(even) → Every even-numbered element (2, 4, 6...)
:nth-child(odd) → Every odd-numbered element (1, 3, 5...)
:nth-child(3n) → Every third element (3, 6, 9...)Visualizing the full selector
Here's a way to read complex selectors — right to left, like unwrapping a gift:
'table tr:nth-child(even)'Start from the right:
:nth-child(even)→ "that are at even positions"tr→ "...table row elements"table→ "...inside a table"
Full sentence: "All table row elements that are inside a table and sit at even-numbered positions."
Practice reading selectors this way and they'll start to feel natural very quickly.
The bigger picture
This example shows why Selector.update() is so powerful for a beginner:
- You don't need to touch your HTML — no extra classes, no
idattributes - You don't need to write loops — the selector handles the counting
- You describe what you want in plain selector language
- The code is self-documenting — the selector tells you exactly what it targets
You focused on the outcome — what the table should look like — not the procedure of how to get there.
That's the mindset shift that makes front-end development feel less like engineering and more like design.