CSSBeginner

CSS Selectors in Depth: Combinators and Attribute Selectors

Learn CSS combinators and attribute selectors to target elements by relationships, children, siblings, and specific HTML attributes.

All CSS lessons

What you will learn

In CSS Syntax and Basic Selectors you learned to target elements by their tag, class and id. That is enough for small pages, but real pages need more precision: “only the paragraph directly inside the card”, “the paragraph right after the heading”, “every link that points to a PDF”. In this lesson you will learn how to combine selectors, how the four combinators (space, >, +, ~) describe relationships in the HTML tree, and how attribute selectors pick elements by their attributes.

The HTML we will style

Every example runs against this small page. Read it slowly and picture the tree: what is inside what, and which elements are neighbours.

<header class="site-header">
  <h1 id="site-title">Learn Web Dev</h1>
  <nav class="main-nav">
    <a href="/html/" class="nav-link active">HTML</a>
    <a href="/css/" class="nav-link">CSS</a>
    <a href="https://example.com" class="nav-link" target="_blank">Partner site</a>
    <a href="syllabus.pdf" class="nav-link">Syllabus (PDF)</a>
  </nav>
</header>

<main class="content">
  <h2 class="section-title">Today's Lesson</h2>
  <p class="intro">Selectors decide which elements get styled.</p>
  <p>This paragraph comes right after the intro.</p>
  <p>This is the second paragraph after the intro.</p>

  <div class="card">
    <h3 class="card-title">Card Title</h3>
    <p>A direct child paragraph of the card.</p>
    <div class="card-footer">
      <p>A nested paragraph, inside the card footer.</p>
    </div>
  </div>

  <form class="signup">
    <input type="text" name="fullname" placeholder="Your name">
    <input type="email" name="email" placeholder="Your email">
    <input type="text" name="phone" placeholder="Phone (optional)" disabled>
  </form>
</main>

The family tree of the <main> part looks like this (indentation means “inside”):

main.content
├── h2.section-title
├── p.intro
├── p                      (sibling after .intro)
├── p                      (sibling after .intro)
├── div.card
│   ├── h3.card-title
│   ├── p                  (direct child of .card)
│   └── div.card-footer
│       └── p              (grandchild of .card)
└── form.signup
    ├── input[type=text]
    ├── input[type=email]
    └── input[type=text][disabled]

Here is a map of the elements we will point at:

Selector target Element in the HTML
.nav-link.active the “HTML” link (it has both classes)
.card p both paragraphs inside the card (direct and nested)
.card > p only the direct child paragraph of the card
.intro + p the paragraph right after .intro
.intro ~ p both paragraphs after .intro
a[target] the “Partner site” link
a[href$=".pdf"] the “Syllabus (PDF)” link
input[type="email"] the email input
input[disabled] the phone input

Combining simple selectors

You can join selectors with no space to say “this AND that on the same element”.

Two classes on one element

/* Matches an element that has BOTH classes */
.nav-link.active {
  background-color: royalblue;
  color: white;
}

Only the “HTML” link has class="nav-link active", so only it becomes blue. The other links have nav-link but not active. Writing .nav-link.active (no space) means “one element with both classes”.

A tag plus a class

/* A <p> that has the class "intro" */
p.intro {
  font-weight: bold;
}

/* Any <a> that has the class "nav-link" */
a.nav-link {
  text-decoration: none;
}

p.intro only matches a <p>. If a <div> also had class="intro", this rule would ignore it. Usually plain .intro is enough, and it is easier to reuse.

Space matters! .card .title (with a space) means “a .title inside a .card”. .card.title (no space) means “one element that is both”. The two look almost the same and behave completely differently.

Combinators: relationships in the HTML tree

A combinator is the symbol between two selectors. It says how the two elements are related. There are four.

1. Descendant combinator (a space)

A B matches every B that is anywhere inside A: a child, a grandchild, a great-grandchild.

/* every <p> anywhere inside .card */
.card p {
  color: seagreen;
}

Both card paragraphs turn green: the direct child and the one nested inside .card-footer. The descendant combinator is the one you will use most.

2. Child combinator (>)

A > B matches every B that is a direct child of A. Grandchildren are ignored.

/* only <p> elements directly inside .card */
.card > p {
  font-weight: bold;
}

Now only “A direct child paragraph of the card” is bold. The paragraph in .card-footer is a grandchild, so it is skipped. Use > when you want to be exact and avoid styling things nested deeper.

3. Adjacent sibling combinator (+)

Siblings are elements that share the same parent. A + B matches the B that comes immediately after A.

/* the one <p> right after .intro */
.intro + p {
  background-color: lightyellow;
}

Only “This paragraph comes right after the intro.” is highlighted. The second paragraph is also a sibling, but it does not directly follow .intro.

4. General sibling combinator (~)

A ~ B matches every B that comes after A, anywhere later under the same parent (not just the next one).

/* every <p> that follows .intro at the same level */
.intro ~ p {
  border-left: 4px solid orange;
  padding-left: 8px;
}

Both paragraphs after the intro get an orange bar. The paragraphs inside .card are not affected, because they are not siblings of .intro. They live one level deeper.

Combinator Written Reads as
descendant A B B anywhere inside A
child A > B B directly inside A
adjacent sibling A + B B immediately after A
general sibling A ~ B any B after A

Sibling selectors only look forward. CSS cannot (with these selectors) style an element “because of” something that comes after it. Later you will meet :has(), which can.

Attribute selectors

Attribute selectors match elements by the HTML attributes they carry. They go in square brackets.

Does the attribute exist?

/* any <a> with a target attribute (the "Partner site" link) */
a[target] {
  font-style: italic;
}

/* any <input> with the disabled attribute (the phone field) */
input[disabled] {
  background-color: #eee;
}

Does the attribute have an exact value?

/* the <input> whose type is exactly "email" */
input[type="email"] {
  border: 2px solid royalblue;
}

Does the value start, end or contain something?

Selector Meaning Matches in our page
[attr^="x"] value starts with x a[href^="https"] finds the external link
[attr$="x"] value ends with x a[href$=".pdf"] finds the syllabus
[attr*="x"] value contains x anywhere a[href*="example"] finds the partner site
/* links to a PDF file */
a[href$=".pdf"] {
  color: crimson;
}

/* links that begin with https (external) */
a[href^="https"] {
  color: darkorange;
}

Attribute selectors are great for styling content you cannot easily add classes to, for example links written by someone else, or all form fields of a certain type. Values that contain spaces or special characters need quotes, so it is a good habit to always quote them.

Add an i before the closing bracket to ignore upper and lower case: a[href$=".PDF" i].

Grouping with combinators

Everything you have learned mixes with grouping (commas):

.card p,
.signup input,
.intro + p {
  font-size: 18px;
}

Each comma-separated part is a complete selector of its own. If one part is invalid, the browser ignores the whole rule, so type carefully.

Keep selectors short

You can chain as many parts as you like:

main.content .card .card-footer p { }   /* too long! */

But long selectors are hard to read, hard to override and break as soon as the HTML changes. A good rule of thumb is two or three parts at most. If you need more, give the element its own class instead:

.card-footer-text { }

Putting it together

.nav-link {
  display: inline-block;
  padding: 8px 14px;
  text-decoration: none;
}

.nav-link.active {
  background-color: royalblue;
  color: white;
}

a[href^="https"] {
  color: darkorange;
}

a[href$=".pdf"] {
  color: crimson;
}

.card p {
  color: seagreen;
}

.card > p {
  font-weight: bold;
}

.intro + p {
  background-color: lightyellow;
}

.intro ~ p {
  border-left: 4px solid orange;
  padding-left: 8px;
}

input[type="email"] {
  border: 2px solid royalblue;
}

input[disabled] {
  background-color: #eeeeee;
}

Common mistakes

  • Adding a space by accident. .nav-link .active looks for an .active inside a .nav-link, which does not exist.
  • Confusing > with a space. A space reaches all descendants, while > reaches only direct children.
  • Expecting + to skip elements. It matches only the very next sibling. Use ~ for “any later sibling”.
  • Forgetting the quotes or the square brackets in attribute selectors: a[href$=.pdf] may fail. Write a[href$=".pdf"].
  • Mixing up ^= and $=. Think “caret = start of a line, dollar = end”, as in regular expressions.
  • Writing very long selectors that break when the HTML changes.
  • One typo inside a grouped selector, which disables the whole rule.

Practice

  1. Click Practice in Editor. Make the .card > p bold and check with DevTools that the footer paragraph is not affected.
  2. Change .card p to .card > p and see which paragraph loses its colour.
  3. Highlight only the paragraph that follows .intro, using +. Then use ~ and compare.
  4. Colour external links (https) orange, PDF links red, and add a small border under the link that has a target attribute.
  5. Style input[type="email"] and input[disabled] differently from each other.
  6. Rewrite this long selector as a shorter one with a new class, and add the class to the HTML: main.content .card .card-footer p.

Recap

  • Join selectors without a space to require several conditions on one element: .nav-link.active, p.intro.
  • Combinators describe the relationship between elements: space (descendant), > (direct child), + (next sibling) and ~ (any later sibling).
  • Attribute selectors use square brackets: [attr], [attr="value"], [attr^="start"], [attr$="end"], [attr*="part"].
  • Grouping with commas works with all of them, but one invalid part cancels the whole rule.
  • Keep selectors short: two or three parts is plenty.
  • Next you will style states and generated content with pseudo-classes and pseudo-elements.