~/
CSS Selectors
Quiz
...

CSS Selectors

beginner · updated Wed Oct 07 2026Contribute

Master combinators, pseudo-classes, pseudo-elements, and advanced selectors.

CSS Selectors

A selector is the part of a CSS rule that says which elements to style. The basics — p, .highlight, #header — are enough for a simple page. Real pages need more: “only the first paragraph inside the article”, “only links that go to a PDF”, “only while the mouse is over it”. In this lesson you will learn how to say all of that, and how the browser decides which rule wins when two of them match.

Basic Selectors Review

Four selectors cover most pages. You met them in the last lesson, and everything else here is built from them.

/* Element selector — every <p> on the page */
p { color: #333; }

/* Class selector — anything with class="highlight" */
.highlight { background-color: yellow; }

/* ID selector — the one element with id="header" */
#header { background-color: #f0f0f0; }

/* Universal selector — every element */
* { box-sizing: border-box; }

The comma in h1, h2, h3 is a grouping selector. It saves you repeating the same declarations when several selectors want the same style:

h1, h2, h3 {
  font-family: Georgia, serif;
}

One thing to remember: these four are not equally strong. * always loses, and #header almost always wins. We come back to that at the end.

Combinators

A combinator joins two selectors by describing how the elements sit in the HTML. Every example below uses this markup:

<div class="card">
  <h2>Title</h2>
  <p>Direct child of the card.</p>
  <div>
    <p>Inside a nested div.</p>
  </div>
</div>

Descendant Selector (space)

Targets all descendants, at any depth:

/* Both <p> elements above */
.card p {
  color: blue;
}

Descendant is the loosest combinator. It only asks “is there a .card somewhere above this p?”, so it matches more than people expect.

Child Selector ( > )

Targets only direct children:

/* Only "Direct child of the card." */
.card > p {
  font-weight: bold;
}

The > means “immediately inside”. The second <p> is two levels down, so it is skipped.

Adjacent Sibling ( + )

Targets the element right after a match:

/* The <p> that comes directly after the <h2> */
h2 + p {
  margin-top: 0;
}

General Sibling ( ~ )

Targets every following element of the same type:

/* Every <p> after the <h2>, including the nested one */
h2 ~ p {
  color: gray;
}

+ grabs one, ~ grabs the rest.

One thing to remember: a combinator does not move anything around. div p does not mean “the p’s inside the div”, it means “every p that has a div above it” — the browser tests each <p> on its own.

Attribute Selectors

You can select by what an attribute contains, not just by tag, class, or ID. That is handy for styling links by type, or flagging inputs, without adding a class to every element.

/* Has the attribute at all */
a[href] { color: navy; }

/* Exact value */
a[href="https://example.com"] { color: red; }

/* Value starts with */
a[href^="https"] { color: teal; }

/* Value ends with */
a[href$=".pdf"] { color: crimson; }

/* Value contains the substring, anywhere */
a[href*="example"] { color: purple; }

/* One word out of a list of words, e.g. class="btn btn-large" */
[class~="large"] { padding: 20px; }

What each symbol means:

  • [href] — the attribute is there, whatever its value.
  • = — the whole value matches exactly.
  • ^= — the value starts with this text. Useful for “secure links only” with [href^="https"].
  • $= — the value ends with this text. Useful for [href$=".pdf"].
  • *= — the value contains this text anywhere. The loosest of the five.
  • ~= — the value is a space-separated list of words and one of them matches. This is how you reach a single class inside class="btn btn-large" without touching the other one.

Each symbol goes inside the same square brackets, and you can combine one with a tag or class: a.link[target="_blank"]::after.

Pseudo-Classes

A pseudo-class adds a condition the browser checks for you: a state like hover, a position like first-child, or a property of the element like required. You style the element itself — no extra class in the HTML.

/* Not visited yet */
a:link { color: blue; }

/* Already clicked */
a:visited { color: purple; }

/* Mouse is over it */
a:hover { color: red; }

/* Being clicked right now */
a:active { color: orange; }

/* Keyboard focus */
a:focus { outline: 2px solid blue; }

Order matters: write them L-V-H-A — Link, Visited, Hover, Active. All five are equally strong, so the browser simply uses the order you wrote them in. Put :hover last and it wins over :link; put it first and :link wins and the hover never shows.

Structural Pseudo-Classes

These ask where an element sits among its siblings:

/* The first <li> */
li:first-child { font-weight: bold; }

/* The last <li> */
li:last-child { border-bottom: none; }

/* The second <li> */
li:nth-child(2) { color: red; }

/* The 1st, 3rd, 5th… <li> */
li:nth-child(odd) { background-color: #f5f5f5; }

/* Every third <li> */
li:nth-child(3n) { font-size: 1.2em; }

/* The first <p> among its siblings */
p:first-of-type { font-size: 1.5em; }

/* An element that is its parent's only child */
.item:only-child { margin: 0 auto; }

/* An element with no children and no text */
div:empty { display: none; }

nth-child takes a formula, which is more useful than it looks:

  • even or 2n — every second item. This is how you stripe table rows.
  • odd or 2n + 1 — every second item, starting from the first.
  • 3n — every third item.
  • 3n + 1 — every third item, starting from the first.

first-of-type is not the same as first-child. first-child means “nothing at all comes before it”, while first-of-type means “no element of the same tag comes before it”. If a <div> sits above the first <p>, that <p> is still first-of-type.

Form Pseudo-Classes

/* Required field */
input:required { border-left: 3px solid #e74c3c; }

/* Passes validation */
input:valid { border-color: green; }

/* Fails validation */
input:invalid { border-color: #e74c3c; }

/* Checkbox or radio that is ticked */
input:checked { accent-color: #3498db; }

/* Greyed out */
input:disabled { background-color: #eee; cursor: not-allowed; }

/* Anything the reader can still type in */
input:enabled { background-color: white; }

:valid and :invalid are live — they update as the reader types, so a field can turn green the moment it is filled in correctly.

The :not() Pseudo-Class

:not() styles everything except something. That is usually easier than listing every case you want:

/* A divider under every list item except the last one */
li:not(:last-child) {
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
}

One rule does the work of two. Anything can go inside the parentheses, including a class: body:not(.dark) { color: #222; }.

Pseudo-Elements

A pseudo-element targets a part of an element, or adds new content to it. A pseudo-class is one colon, like :hover. A pseudo-element is two, like ::before.

<!-- Html Elements -->
<!-- ------------- -->
<h1>This is heading</h1>
<p>this is paragraph</p>
<input placeholder="This is input" />

<!-- Styles -->
<!-- ------ -->
<style>

/* The first rendered line of text */
p::first-line { font-weight: bold; }

/* The first letter */
p::first-letter { font-size: 2em; float: left; }

/* Content added before the element */
h1::before { content: "★ "; color: gold; }

/* Content added after the element */
h1::after { content: " ★"; color: gold; }

/* Placeholder hint inside an empty field */
input::placeholder { color: red; font-style: italic; }

</style>

::before and ::after generate content that is not in your HTML, and content is required — without it the rule does nothing. Only a few kinds of value work in content:

  • a string in quotes — content: "→"
  • an empty string — content: "", useful as a positioning hook
  • open-quote and close-quote — real typographic quotes that follow the page’s language
  • attr(name) — text pulled out of an attribute

That last one saves a lot of repetition:

/* Mark every link that leaves the site */
a[href^="https"]::after {
  content: " ↗";
  font-size: 0.8em;
}

Note: ::placeholder styles the hint text only. input:placeholder-shown is the matching state — it matches the input itself while its placeholder is on screen.

One thing to remember: content added with ::before or ::after is not in your HTML. You cannot select it, restyle its parts, or change it from JavaScript, so use a real element whenever the content carries meaning.

Selector Specificity

When two rules set the same property on the same element, the browser compares specificity first. If the scores are equal, the rule written last in the stylesheet wins.

Selector Example Score
Element p, div, a 1
Class, attribute, pseudo-class .text, a[href], :hover 10
ID #header 100
Inline style style="color: red" 1000
!important color: red !important beats all of the above

So a <p class="text"> ends up blue: .text scores 10, p scores 1, and #header does not match it at all.

p { color: black; }      /* 1   */
.text { color: blue; }   /* 10  */
#header { color: red; }  /* 100 */

The real rule is easier than the numbers suggest: count IDs first, then classes, then elements, and compare left to right. One ID always beats a hundred classes.

Avoid !important. It skips the comparison entirely, which means you can no longer fix a problem by editing the rule you actually care about. You reach for it when you have already lost — almost always because some other selector is too specific.

Complete Example

Here is one small page that uses everything from this lesson: combinators, grouping, a link state, structural and form pseudo-classes, :not(), attribute selectors, and a pseudo-element.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>CSS Selectors</title>
    <style>
      /* Grouping selector */
      h1, h2 {
        font-family: Georgia, serif;
        color: #2c3e50;
      }

      /* Descendant selector */
      .nav a {
        display: inline-block;
        padding: 10px 20px;
        color: #555;
        text-decoration: none;
      }

      /* Hover state on the same links */
      .nav a:hover {
        background-color: #3498db;
        color: white;
      }

      /* Attribute selector: mark links that leave the site */
      a[href^="https"]::after {
        content: " ↗";
        font-size: 0.8em;
      }

      /* Adjacent sibling: no gap under a heading that hugs a paragraph */
      h2 + p {
        margin-top: 0;
      }

      /* Structural: stripe the odd rows */
      tr:nth-child(odd) {
        background-color: #f9f9f9;
      }

      /* :not() — a divider under every list item but the last */
      .list li:not(:last-child) {
        border-bottom: 1px solid #eee;
        padding-bottom: 10px;
        margin-bottom: 10px;
      }

      /* Form state: the empty email field is flagged before typing */
      input:required {
        border-left: 3px solid #e74c3c;
      }

      input:focus {
        outline: none;
        border-color: #3498db;
      }

      /* Pseudo-element: real typographic quotes */
      .quote::before {
        content: open-quote;
        font-size: 3em;
        color: #ccc;
        line-height: 0;
        vertical-align: -0.4em;
      }

      /* Hide the wrapper when the list has nothing in it */
      .list:empty {
        display: none;
      }
    </style>
  </head>
  <body>
    <nav class="nav">
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="https://example.com">External</a>
    </nav>

    <h1>Plans</h1>
    <h2>Prices</h2>
    <p>Everything starts at the same price.</p>

    <table>
      <tr><td>Starter</td><td>$0</td></tr>
      <tr><td>Pro</td><td>$19</td></tr>
      <tr><td>Team</td><td>$49</td></tr>
    </table>

    <ul class="list">
      <li>One seat</li>
      <li>Email support</li>
      <li>Cancel any time</li>
    </ul>

    <form>
      <input type="email" placeholder="you@example.com" required />
    </form>

    <blockquote class="quote">
      Selectors are how you talk about elements.
    </blockquote>
  </body>
</html>

What this means:

  • .nav a:hover is a descendant selector plus a state. It beats .nav a because it is written later and both have the same score.
  • h2 + p only kills the paragraph that sits directly under the heading. Every other paragraph keeps its default margin.
  • tr:nth-child(odd) stripes by position, so inserting a row re-stripes automatically. No odd or even class in the HTML.
  • .list li:not(:last-child) draws a separator between items without a border on the bottom of the list.
  • a[href^="https"]::after adds the arrow purely in CSS, so the HTML stays free of decorative markup.

Best Practices

  1. Keep selectors simple — Every extra element or class makes the rule harder to read and easier to break
  2. Use classes — Reusable, and they do not fight with JavaScript the way IDs can
  3. Avoid !important — Fix the specificity instead; it is usually a one-line change
  4. Use BEM naming — .block__element--modifier keeps names readable and collision-free
  5. Test in multiple browsers — Especially :focus-visible, which is the newest of these

Common Mistakes

  1. Overly specific selectors — #main > div > ul > li > a breaks the moment the HTML changes
  2. Using !important — Hides the real problem instead of fixing it
  3. Forgetting link order — L-V-H-A order matters; :hover before :link never shows
  4. Not using pseudo-classes — Missing hover and focus states leaves the page feeling dead
  5. Overusing IDs — Hard to override later, because almost nothing outranks an ID