Skip to content

Sponsor

Sponsor

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 class
  • tag → 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:

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

javascript
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)

javascript
'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 targeted

What 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)

javascript
'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 background

Wait — 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

javascript
'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

javascript
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:

  1. Paint all odd rows white
  2. Paint all even rows gray
  3. 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

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

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)

javascript
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:

  1. :nth-child(even) → "that are at even positions"
  2. tr → "...table row elements"
  3. 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 id attributes
  • 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.