Advanced CSS Selectors

Reviewed & published by Brayan K

Advanced CSS selectors target elements by their relationships and state instead of just their class or tag, using combinators like > and ~, attribute selectors such as [type="email"], and pseudo-classes like :nth-child() to style markup precisely without extra classes.

Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

💡 Think of It Like This

Basic selectors are like addressing a letter to "John". Advanced selectors are like saying "the third John who lives on Main Street, but only when he's at home." They give you surgical precision — you can target any element based on its state, position, attributes, or relationship to other elements.

1. Pseudo-classes — Targeting Element States

Pseudo-classes select elements based on their current state — whether they're hovered, focused, the first child, or match a pattern.

CategorySelectorWhat It Targets
Interactive:hoverMouse is over element
:focusElement has keyboard focus
:activeElement is being clicked
:visitedLink has been visited
Position:first-childFirst child of parent
:last-childLast child of parent
:nth-child(n)Nth child (odd, even, 3n, 2n+1)
:not(selector)Everything except the selector
Form:checkedChecked checkbox/radio
:valid / :invalidForm validation state
:disabledDisabled form element
<!DOCTYPE html>
<html>
<head>
  <title>Pseudo-classes</title>
  <style>
    body {
      background: #0f172a;
      font-family: system-ui, sans-serif;
      padding: 30px;
      color: #e5e7eb;
    }

    h3 { color: #22c55e; margin: 25px 0 10px; }

    /* Styled list with nth-child */
    ul { list-style: none; padding: 0; max-width: 400px; }

    li {
      padding: 12px 16px;
      border-radius: 6px;
      margin: 4px 0;
      transition: all 0.2s;
    }

    /* Alternate row colours */
    li:nth-child(odd) { background: #1e293b; }
    li:nth-child(even) { background: #334155; }

    /* First and last child */
    li:first-child {
      border-left: 4px solid #3b82f6;
      font-weight: bold;
    }

    li:last-child {
      border-left: 4px solid #22c55e;
    }

    /* Hover effect (but not first child) */
    li:not(:first-child):hover {
      background: #3b82f6;
      color: white;
      transform: translateX(5px);
    }

    /* Link states */
    .links a {
      display: inline-block;
      padding: 10px 20px;
      margin: 5px;
      background: #1e293b;
      color: #94a3b8;
      text-decoration: none;
      border-radius: 6px;
      transition: all 0.2s;
    }

    .links a:hover { background: #3b82f6; color: white; }
    .links a:active { transform: scale(0.95); }

    /* Form states */
    input {
      background: #1e293b;
      border: 2px solid #334155;
      color: white;
      padding: 10px;
      border-radius: 6px;
      margin: 5px 0;
      width: 300px;
      display: block;
    }

    input:focus { border-color: #3b82f6; outline: none; }
    input:valid { border-color: #22c55e; }
    input:invalid { border-color: #ef4444; }

    .note { color: #64748b; font-size: 13px; margin-top: 5px; }
  </style>
</head>
<body>
  <h1>Pseudo-classes Demo</h1>

  <h3>:nth-child, :first-child, :last-child, :not()</h3>
  <ul>
    <li>📌 First item (blue border, bold)</li>
    <li>Second item (hover me!)</li>
    <li>Third item (hover me!)</li>
    <li>Fourth item (hover me!)</li>
    <li>✅ Last item (green border)</li>
  </ul>
  <p class="note">Odd = dark, Even = lighter. :not(:first-child) gets hover effect.</p>

  <h3>:hover and :active</h3>
  <div class="links">
    <a href="#">Hover Me</a>
    <a href="#">Click Me</a>
    <a href="#">Try Me</a>
  </div>

  <h3>:focus, :valid, :invalid</h3>
  <input type="email" placeholder="Enter email (try valid/invalid)" required>
  <p class="note">Focus = blue border. Valid email = green. Invalid = red.</p>
  <!-- ✅ Expected result, measured in a real browser:
     ul -> list-style-type: none
     .links a -> display: inline-block
     input -> display: block
     .note -> count: 2
  -->
</body>
</html>
The page this code makes: See :hover, :nth-child, :first-child, and :not() at work
What this code shows in a browser window 720 pixels wide.

2. Pseudo-elements — Virtual Content

Pseudo-elements create virtual elements that don't exist in the HTML. They're perfect for decorative content, icons, and styling parts of text.

Pseudo-elementWhat It Does
::beforeInserts content before element (requires content)
::afterInserts content after element (requires content)
::first-letterStyles only the first letter of text
::first-lineStyles only the first line of a paragraph
::selectionStyles highlighted/selected text
<!DOCTYPE html>
<html>
<head>
  <title>Pseudo-elements</title>
  <style>
    body {
      background: #0f172a;
      font-family: system-ui, sans-serif;
      padding: 30px;
      color: #e5e7eb;
    }

    h3 { color: #22c55e; margin: 25px 0 10px; }

    /* ::before and ::after */
    .quote {
      background: #1e293b;
      padding: 20px 25px;
      border-radius: 8px;
      font-style: italic;
      font-size: 1.1rem;
      position: relative;
      margin: 15px 0;
    }

    .quote::before {
      content: "❝";
      font-size: 3rem;
      color: #3b82f6;
      position: absolute;
      top: -10px;
      left: 10px;
    }

    .quote::after {
      content: "❞";
      font-size: 3rem;
      color: #3b82f6;
      position: absolute;
      bottom: -20px;
      right: 10px;
    }

    /* ::first-letter - drop cap */
    .drop-cap {
      background: #1e293b;
      padding: 20px;
      border-radius: 8px;
      line-height: 1.7;
      margin: 15px 0;
    }

    .drop-cap::first-letter {
      font-size: 3.5rem;
      font-weight: bold;
      color: #3b82f6;
      float: left;
      margin-right: 10px;
      line-height: 1;
    }

    /* ::selection */
    ::selection {
      background: #3b82f6;
      color: white;
    }

    /* Labels with ::before */
    .tag {
      display: inline-block;
      padding: 6px 14px;
      background: #1e293b;
      border-radius: 20px;
      font-size: 14px;
      margin: 5px;
    }

    .tag.new::before { content: "🆕 "; }
    .tag.hot::before { content: "🔥 "; }
    .tag.sale::before { content: "💰 "; }

    /* Required field asterisk */
    .required::after {
      content: " *";
      color: #ef4444;
      font-weight: bold;
    }

    .note { color: #64748b; font-size: 13px; margin-top: 5px; }
  </style>
</head>
<body>
  <h1>Pseudo-elements</h1>

  <h3>::before and ::after — Decorative quotes</h3>
  <div class="quote">
    The only way to do great work is to love what you do.
  </div>

  <h3>::first-letter — Drop cap</h3>
  <div class="drop-cap">
    Once upon a time in the land of CSS, developers discovered they could style individual letters and lines without adding extra HTML elements. This changed everything about web typography.
  </div>

  <h3>::before for icons/labels</h3>
  <div>
    <span class="tag new">Feature</span>
    <span class="tag hot">Trending</span>
    <span class="tag sale">Discount</span>
  </div>

  <h3>::after for required markers</h3>
  <p><span class="required">Email</span></p>
  <p><span class="required">Password</span></p>

  <h3>::selection — Try selecting this text!</h3>
  <p>Highlight me to see custom selection colours (blue background, white text).</p>
  <!-- ✅ Expected result, measured in a real browser:
     .quote -> position: relative
     .tag -> display: inline-block
     .tag -> count: 3
  -->
</body>
</html>
The page this code makes: Add decorative content and style text parts without extra HTML
What this code shows in a browser window 720 pixels wide.

3. Combinators — Relationship Selectors

Combinators target elements based on their relationship to other elements in the DOM tree.

CombinatorSyntaxTargets
Descendantdiv pAll p elements inside div (any depth)
Childdiv > pOnly direct child p elements of div
Adjacent siblingh2 + pFirst p immediately after h2
General siblingh2 ~ pAll p elements after h2 (same parent)

🧠 Worked Example — which paragraph does each combinator actually hit?

The table above tells you what each combinator means. This example shows you which elements it really matches on a page that is shaped like a real page — with one paragraph tucked inside a <div>, which is where nearly every combinator bug comes from. Four paragraphs, six rules; every rule's actual victims are listed in the comments.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Worked Example: Combinators</title>
<style>
  * { box-sizing:border-box; margin:0; padding:0; }
  body { background:#0f172a; color:#e5e7eb; font-family:system-ui,sans-serif; padding:24px; }
  .demo { background:#1e293b; border-radius:10px; padding:16px; }
  h2 { font-size:1rem; color:#e5e7eb; margin-bottom:10px; }
  p { font-size:.9rem; margin:6px 0; padding:8px 10px; background:#334155; border-radius:6px; }
  .wrap { border:1px dashed #64748b; border-radius:8px; padding:10px; margin:8px 0; }

  /* DESCENDANT (a space): any p anywhere inside .demo, however deeply buried.
     Hits P1, P2, P3, P4 — including P3, which is inside the dashed div. */
  .demo p          { color:#94a3b8; }

  /* CHILD (>): only p elements that are DIRECT children of .demo.
     Hits P1, P2, P4. P3 misses out — its parent is .wrap, not .demo. */
  .demo > p        { border-left:4px solid #3b82f6; }

  /* ADJACENT SIBLING (+): the one p that comes immediately after the h2.
     Hits P1 only. */
  h2 + p           { background:#1d4ed8; }

  /* GENERAL SIBLING (~): every p after the h2 that shares its parent.
     Hits P1, P2, P4. P3 is not a sibling of the h2, so it stays light. */
  h2 ~ p           { font-weight:700; }

  /* THE CLASSIC TRAP: :nth-child counts ALL the children, not just the p's.
     The children of .demo are  h2(1)  P1(2)  P2(3)  .wrap(4)  P4(5).
     The odd ones are 1, 3 and 5 — so this hits P2 and P4, NOT P1.
     If you want "every other paragraph", use :nth-of-type(odd) instead. */
  .demo > p:nth-child(odd) { outline:2px solid #f59e0b; }

  /* :not() takes any simple selector and inverts it. Hits P1, P2, P3. */
  .demo p:not(.skip) { text-decoration:none; }
  .skip            { opacity:.5; }
</style>
</head>
<body>
<section class="demo">
  <h2>Heading</h2>
  <p>P1 — right after the h2</p>
  <p>P2 — a later sibling</p>
  <div class="wrap"><p>P3 — nested inside a div</p></div>
  <p class="skip">P4 — has class "skip"</p>
</section>

<!-- ✅ What you should see, paragraph by paragraph:
     P1 — solid blue background, bold text, blue left bar, no orange outline.
     P2 — grey background, bold, blue left bar, AND an orange outline.
     P3 — grey background, NOT bold, NO blue left bar (it is not a direct child,
          and not a sibling of the h2). This is the paragraph that proves the
          difference between a space and a > in a selector.
     P4 — grey, bold, blue left bar, orange outline, and faded to 50% by .skip.
     Now change  .demo > p:nth-child(odd)  to  .demo > p:nth-of-type(odd)  and the
     orange outline moves from P2 and P4 onto P1 and P4 — because nth-of-type
     ignores the h2 and the div and counts only the paragraphs it is sitting
     among: P1 is the 1st, P2 the 2nd, P4 the 3rd. Run it and check. -->
  <!-- ✅ Expected result, measured in a real browser:
     .demo p:not(.skip) -> text-decoration-line: none
     .skip -> opacity: 0.5
  -->
</body></html>
The page this code makes: Six selectors over the same four paragraphs, with the matches spelled out
What this code shows in a browser window 720 pixels wide.

4. Attribute Selectors

Target elements based on their HTML attributes — incredibly useful for styling links, inputs, and data attributes.

SelectorMatchesExample Use
[href]Has the attributeAny element with href
[type="email"]Exact matchEmail inputs only
[href*="youtube"]Contains substringLinks containing "youtube"
[href^="https"]Starts withSecure links
[href$=".pdf"]Ends withPDF download links
<!DOCTYPE html>
<html>
<head>
  <title>Attribute Selectors</title>
  <style>
    body {
      background: #0f172a;
      font-family: system-ui, sans-serif;
      padding: 30px;
      color: #e5e7eb;
    }

    h3 { color: #22c55e; margin: 25px 0 10px; }

    .links { display: flex; flex-direction: column; gap: 8px; max-width: 400px; }

    a {
      display: block;
      padding: 12px 16px;
      background: #1e293b;
      color: #94a3b8;
      text-decoration: none;
      border-radius: 6px;
      border-left: 4px solid #334155;
      transition: all 0.2s;
    }

    /* External links (start with https) */
    a[href^="https"] {
      border-left-color: #3b82f6;
    }
    a[href^="https"]::after {
      content: " 🔗";
    }

    /* PDF links */
    a[href$=".pdf"] {
      border-left-color: #ef4444;
    }
    a[href$=".pdf"]::after {
      content: " 📄";
    }

    /* YouTube links */
    a[href*="youtube"] {
      border-left-color: #ef4444;
      background: #1c1917;
    }
    a[href*="youtube"]::after {
      content: " ▶️";
    }

    /* Email inputs */
    input[type="email"] {
      background: #1e293b;
      border: 2px solid #3b82f6;
      color: white;
      padding: 10px;
      border-radius: 6px;
      width: 300px;
    }

    input[type="password"] {
      background: #1e293b;
      border: 2px solid #f59e0b;
      color: white;
      padding: 10px;
      border-radius: 6px;
      width: 300px;
    }

    .note { color: #64748b; font-size: 13px; margin-top: 5px; }
  </style>
</head>
<body>
  <h1>Attribute Selectors</h1>

  <h3>Links styled by href attribute</h3>
  <div class="links">
    <a href="#internal">Internal link (no icon)</a>
    <a href="https://google.com">External link [href^="https"]</a>
    <a href="https://youtube.com/watch">YouTube link [href*="youtube"]</a>
    <a href="report.pdf">Download PDF [href$=".pdf"]</a>
  </div>

  <h3>Inputs styled by type attribute</h3>
  <p><input type="email" placeholder="[type='email'] — blue border"></p>
  <p><input type="password" placeholder="[type='password'] — amber border"></p>
  <!-- ✅ Expected result, measured in a real browser:
     .links -> display: flex
     a -> display: block
  -->
</body>
</html>
The page this code makes: Style links, inputs, and elements based on their attributes
What this code shows in a browser window 720 pixels wide.

🎯 Your Turn — style a list and a set of links, without touching the HTML

This is the whole point of good selectors: the HTML below has no extra classes for striping, no "insecure" class on the bad link, no "pdf" class on the download. You add all four effects from CSS alone. Four blanks, marked ___.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn: Selectors</title>
<style>
  /* 🎯 YOUR TURN — fill in the four blanks marked ___ */

  * { box-sizing:border-box; margin:0; padding:0; }
  body { background:#0f172a; color:#e5e7eb; font-family:system-ui,sans-serif; padding:24px; }
  h2 { font-size:.95rem; color:#60a5fa; margin:18px 0 8px; }
  .tasks { list-style:none; max-width:360px; }
  .tasks li { padding:8px 12px; border-radius:6px; }
  .tasks li.done { color:#64748b; text-decoration:line-through; }
  .links a { display:block; color:#93c5fd; margin:4px 0; }

  /* 1) Zebra-stripe the list: shade the 2nd, 4th, 6th... item. */
  .tasks li:nth-child(___) { background:#1e293b; }
  /* 👉 replace ___ with  even  */

  /* 2) Make only the tasks that are NOT finished bold. */
  .tasks li:not(___) { font-weight:700; }
  /* 👉 replace ___ with  .done  */

  /* 3) Turn any link that STARTS WITH an insecure http:// address red.
        Careful: "http" on its own would also match https. */
  .links a[href^="___"] { color:#ef4444; }
  /* 👉 replace ___ with  http://  */

  /* 4) Any link whose address ENDS WITH .pdf gets a "(PDF)" marker after it. */
  .links a[href$="___"]::after { content:" (PDF)"; color:#f59e0b; }
  /* 👉 replace ___ with  .pdf  */
</style>
</head>
<body>
  <h2>Tasks</h2>
  <ul class="tasks">
    <li>Wash up</li>
    <li class="done">Buy milk</li>
    <li>Write CSS</li>
    <li class="done">Feed cat</li>
    <li>Ship it</li>
  </ul>

  <h2>Links</h2>
  <p class="links">
    <a href="https://example.com">Secure link</a>
    <a href="http://example.com">Insecure link</a>
    <a href="/about">Internal link</a>
    <a href="report.pdf">Quarterly report</a>
  </p>

  <!-- ✅ Expected once all four blanks are filled:
       Tasks: "Buy milk" and "Feed cat" (items 2 and 4) sit on a darker band.
              "Wash up", "Write CSS" and "Ship it" are bold; the two struck-through
              done tasks are not.
       Links: "Insecure link" is red. "Secure link" stays blue — if it went red
              too, you used  http  instead of  http://  .
              "Quarterly report" reads "Quarterly report (PDF)" in orange; none of
              the other three get a marker. -->
</body></html>

🧩 Mini-Challenge — a form that reacts, with no JavaScript

Combine a state pseudo-class with a sibling combinator and you get behaviour, not just decoration — the checkbox hack that powers menus, accordions and "show more" panels all over the web. Outline only; you write the CSS.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini-Challenge: Reactive Form</title>
<style>
  /* 🧩 MINI-CHALLENGE: no JavaScript anywhere
     1. .panel — hide it by default with  max-height: 0; overflow: hidden;
        and add  transition: max-height .3s ease;  so it slides.
     2. Reveal it when the checkbox is ticked, using a sibling combinator that
        reaches from the checkbox across to the panel:
              #toggle:checked ~ .panel { max-height: 200px; }
        Read that as: "any .panel later in the same parent as a ticked #toggle".
        It has to be ~ and not +, because the <label> sits between them and +
        only ever reaches the very next sibling.
     3. Do the same to the status line with  #toggle:checked ~ .status  — change
        its colour so it turns green when the panel is open.
     4. Give the email box live feedback with  input[type="email"]:user-invalid
        (red border) and  :user-valid  (green border).
     5. Add  label:focus-within  or an outline on  #toggle:focus-visible  so
        keyboard users can see what is focused.

     ✅ How to check it: tick the checkbox and the hidden panel slides open; untick
        it and it slides shut — with no script on the page. The status line changes
        colour at the same moment, even though there are two elements between it
        and the checkbox, because ~ reaches every later sibling, not just the next.
        If nothing happens when you tick it, you almost certainly used + instead
        of ~ and are styling the <label>.
        Type "abc" in the email box and Tab away: red border. Fix it: green.
  */

  * { box-sizing:border-box; margin:0; padding:0; }
  body { background:#0f172a; color:#e5e7eb; font-family:system-ui,sans-serif; padding:24px; }
  form { max-width:380px; background:#1e293b; padding:18px; border-radius:12px; }
  label { cursor:pointer; font-weight:700; color:#60a5fa; display:inline-block; margin-bottom:10px; }
  input[type="email"] { width:100%; padding:9px 10px; background:#334155; border:1px solid #475569; border-radius:6px; color:#fff; }
  .status { margin-top:12px; font-size:.85rem; }

  /* your CSS here */

</style>
</head>
<body>
  <form>
    <!-- Markup done for you. Note the ORDER: the checkbox comes before the panel,
         which comes before the status line. Sibling combinators only ever look
         forwards, so this order is what makes the CSS possible. -->
    <input type="checkbox" id="toggle">
    <label for="toggle">Show advanced options</label>

    <div class="panel">
      <p style="padding:10px 0;">Advanced options live in here.</p>
      <input type="email" placeholder="Notify this address">
    </div>

    <p class="status">Advanced options are hidden.</p>
  </form>
  <!-- ✅ Expected result, measured in a real browser:
     label -> display: inline-block
     form -> max-width: 380px
  -->
</body></html>

⚠️ Common Mistakes

🎉 Lesson Complete

You now have surgical precision when targeting CSS elements:

Practice quiz

What is the difference between a single colon and a double colon, as in :hover vs ::before?

  • They are interchangeable
  • Double colons are invalid CSS
  • A single colon marks a pseudo-class (state); a double colon marks a pseudo-element (virtual content)
  • Single colons only work on links

Answer: A single colon marks a pseudo-class (state); a double colon marks a pseudo-element (virtual content). Pseudo-classes (:hover, :focus) target a state; pseudo-elements (::before, ::after) create virtual content.

What does the child combinator div > p select?

  • Only p elements that are direct children of div
  • All p elements anywhere inside div
  • The first p after a div
  • All p elements that follow div

Answer: Only p elements that are direct children of div. The > combinator matches only direct (immediate) child p elements of div, not deeper descendants.

What does the descendant combinator div p (with a space) select?

  • Only direct child p elements
  • The p immediately after div
  • div elements inside p
  • All p elements inside div at any depth

Answer: All p elements inside div at any depth. A space is the descendant combinator: it matches all p inside div regardless of nesting depth.

What does the adjacent sibling combinator h2 + p select?

  • All p siblings after h2
  • The first p immediately after an h2 (same parent)
  • Every p inside h2
  • The p before h2

Answer: The first p immediately after an h2 (same parent). h2 + p matches the single p that immediately follows an h2 and shares its parent.

What does the general sibling combinator h2 ~ p select?

  • All p elements after h2 that share the same parent
  • Only the first p after h2
  • p elements inside h2
  • The p directly before h2

Answer: All p elements after h2 that share the same parent. h2 ~ p matches every p that comes after an h2 and has the same parent.

Which attribute selector matches links whose href ends with .pdf?

  • [href^=".pdf"]
  • [href*=".pdf"]
  • [href$=".pdf"]
  • [href~=".pdf"]

Answer: [href$=".pdf"]. $= matches values that end with the substring, so [href$=".pdf"] targets PDF links.

Which attribute selector matches links whose href starts with https?

  • [href$="https"]
  • [href^="https"]
  • [href*="https"]
  • [href="https"]

Answer: [href^="https"]. ^= matches values that start with the substring, so [href^="https"] targets secure links.

Which attribute selector matches an href that contains the substring 'youtube' anywhere?

  • [href^="youtube"]
  • [href$="youtube"]
  • [href="youtube"]
  • [href*="youtube"]

Answer: [href*="youtube"]. *= matches values containing the substring anywhere within them.

What must you always include for a ::before or ::after pseudo-element to render?

  • A width property
  • The content property (even if empty: content: "")
  • A position value
  • A z-index

Answer: The content property (even if empty: content: ""). ::before and ::after require the content property; without it they do not render at all.

What is the difference between :nth-child(2) and :nth-of-type(2)?

  • They are identical
  • :nth-of-type counts from the end
  • :nth-child(2) targets the 2nd child of any type; :nth-of-type(2) targets the 2nd element of that specific type
  • :nth-child only works on lists

Answer: :nth-child(2) targets the 2nd child of any type; :nth-of-type(2) targets the 2nd element of that specific type. :nth-child counts among all siblings; :nth-of-type counts only siblings of the same element type.

Continue this course