CSS Combinators and Attribute Selectors

Target elements by their relationship to other elements, and by their attributes: descendant, child, sibling and attribute selectors with examples.

Lesson 5 of 49Beginner15 min read

Basic selectors pick elements by tag, class or id. That works well, but real pages need more precise targeting. You might want to style only the links inside the footer, only the paragraph that comes straight after a heading, or every input whose type is email. Adding a class to each of those elements by hand is slow and easy to forget.

Combinators solve the first two problems. They combine two selectors and describe a relationship between them: inside, directly inside, right after, or anywhere after. Attribute selectors solve the third. They match elements by the attributes they carry, such as href, type, lang or data-* attributes, and can even match part of an attribute's value.

In this lesson you'll learn the four CSS combinators, the seven forms of attribute selector, and how to read a long selector from right to left. These are some of the most searched CSS selector topics, and once they click you'll write shorter, cleaner HTML with far fewer extra classes.

The four combinators

CombinatorExampleMatches
Descendant (space)nav aEvery a anywhere inside a nav
Child >ul > lili elements that are direct children of a ul
Next sibling +h2 + pThe first p that comes immediately after an h2
Subsequent sibling ~h2 ~ pEvery p after an h2 that shares the same parent

Siblings are elements with the same parent. The + and ~ combinators only look forwards: they never match an element that comes before the first one.

Descendant vs child

The difference matters with nested content. Take a menu with a sub-menu:

HTML
<ul class="menu">
  <li>Home</li>
  <li>Shop
    <ul><li>Shoes</li></ul>
  </li>
</ul>

.menu li matches all three list items, including "Shoes" in the nested list. .menu > li matches only "Home" and "Shop", because "Shoes" is a grandchild, not a child. Use the child combinator when you want a style to stop at the first level.

Attribute selectors

SelectorMatches elements whose attribute…
[title]exists, with any value
[type="email"]equals exactly email
[href^="https"]starts with https
[href$=".pdf"]ends with .pdf
[class*="btn"]contains btn anywhere
[class~="card"]contains the whole word card in a space-separated list
[lang|="en"]is en or starts with en-

Add i before the closing bracket to ignore case: [href$=".pdf" i] also matches .PDF. Quotes around the value are optional for simple words but always safe to use.

Reading selectors from right to left

Browsers read a selector from the right. .card > p a means: find every a, keep those inside a p, then keep those whose p is a direct child of .card. Reading the same way helps you understand long selectors quickly. It also explains a performance tip: the rightmost part, called the key selector, should be reasonably specific. In practice modern browsers are so fast that readability matters more than speed, so keep selectors short mainly so they're easy to maintain.

CSS Combinators Syntax

Combinator and attribute syntax

article p { }           /* descendant */
ul > li { }             /* child */
h2 + p { }              /* next sibling */
h2 ~ p { }              /* all following siblings */
[disabled] { }          /* has attribute */
input[type="email"] { } /* exact value */
a[href^="https"] { }    /* starts with */
a[href$=".pdf" i] { }   /* ends with, any case */
[class*="icon-"] { }    /* contains */

CSS Combinators Summary

  • A space selects descendants at any depth; > selects direct children only.
  • + selects the very next sibling; ~ selects all following siblings.
  • Attribute selectors match by attribute presence, exact value, or start (^=), end ($=) or part (*=) of the value.
  • Add i inside the brackets for a case-insensitive match.
  • Read selectors from right to left, and keep them short.

CSS Combinators Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Child vs descendant in a nested menu

Change > on line 1 to a space and run again. The nested item gets the border too.

.menu > li {
  border-left: 4px solid #16a34a;
  padding-left: 8px;
  margin: 6px 0;
}

.menu {
  list-style: none;
  padding: 0;
  font-family: sans-serif;
}
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 1Only list items that are direct children of .menu get the green bar.
  2. Lines 2 to 4A green bar and spacing. The nested Shoes and Bags items keep the default look.
  3. Lines 7 to 11Remove bullets and padding from the outer list.
Example 2

Sibling combinators for article spacing

article { font-family: Georgia, serif; max-width: 520px; }

h2 + p {
  font-size: 1.2em;
  color: #374151;
  margin-top: 0;
}

h2 ~ p {
  line-height: 1.6;
}
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 7h2 + p matches only the paragraph straight after the heading, giving a larger lead paragraph.
  2. Lines 9 to 11h2 ~ p matches every paragraph after the heading.
Example 3

Styling links and inputs by attribute

a[href^="https"]::after { content: " ↗"; }
a[href$=".pdf" i]::after { content: " (PDF)"; }
a[href^="mailto:"] { color: #b45309; }

input { display: block; margin: 6px 0; padding: 8px; }
input[type="email"] { border: 2px solid #2563eb; }
input[disabled] { background: #f3f4f6; cursor: not-allowed; }
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 1Links that start with https get an arrow, marking them as external.
  2. Line 2The i flag matches .PDF as well as .pdf.
  3. Line 3Email links get their own colour.
  4. Line 6Only the email input gets a blue border.
  5. Line 7Any input with the disabled attribute looks inactive.

Common CSS Combinators Mistakes

Expecting + to look backwards

Don't
/* trying to style the h2 that comes before a p */
p + h2 { color: red; }
Do
/* sibling combinators only look forwards; style the p after the h2 */
h2 + p { color: red; }
/* or use :has(), covered in Modern Selectors */
h2:has(+ p) { color: red; }

Spaces inside attribute selectors

Don't
input [type="email"] { border-color: blue; }
Do
input[type="email"] { border-color: blue; }

A space is the descendant combinator, so the wrong version looks for an element with type="email" inside an input, which never exists.

Using *= when you mean a whole class

Don't
[class*="btn"] { padding: 10px; } /* also matches "submit-btn-wrapper" */
Do
.btn { padding: 10px; }

For classes, a normal class selector is clearer and safer. Save *= for patterns like [class*="icon-"].

CSS Combinators in Real Projects

Where you'll see these selectors

Design systems use attribute selectors to style states without extra classes: [aria-expanded="true"] for open menus, [aria-current="page"] for the active nav link and [data-variant="danger"] for red buttons. Blogs use h2 + p for lead paragraphs, and reset stylesheets use ul[role="list"] to remove bullets only from lists marked as lists. Testing tools such as Playwright also locate elements with the same selector syntax.

CSS Combinators Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Style only the list items that are direct children of .steps.

.steps  li {
  font-weight: bold;
}
Multiple choiceeasy

Which selector matches every link whose address ends in .pdf?

Multiple choicemedium

What does h3 ~ p select?

Multiple choicemedium

Which selector matches <input type="EMAIL"> and <input type="email">?

Fix the bugmedium

Only the paragraph right after each heading should be bold, and only email inputs should have a blue border. Fix the selectors.

h2 ~ p { font-weight: bold; }
input [type=email] { border: 2px solid rgb(37, 99, 235); }
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Lead paragraph is bold
  • Second paragraph is not bold
  • Email input has the blue border
  • Text input is unchanged

CSS Combinators Quiz

5 questions to check what you learned.

  1. What does a space between two selectors mean, as in footer a?

    • The elements are siblings
    • Any a inside a footer, at any depth
    • Only direct children
    • Both selectors match the same element
  2. Which combinator selects only direct children?

    • ~
    • >
    • +
    • space
  3. What does [data-state] match?

    • Elements whose class is data-state
    • Only elements where data-state is empty
    • Nothing, the value is missing
    • Any element with a data-state attribute, whatever its value
  4. Which selector matches links that start with https?

    • a[href^="https"]
    • a[href$="https"]
    • a[href~="https"]
    • a:https
  5. How does the browser read .card > p a?

    • Left to right, starting with .card
    • It checks all three at random
    • Right to left: find every a, then check it's in a p that is a child of .card
    • Only the first part matters

CSS Combinators Challenge

Style a documentation page

  1. Make every li that is a direct child of .toc have font-weight: bold
  2. Give the paragraph straight after each h2 the style color: rgb(55, 65, 81)
  3. Give links whose href ends in .zip a color of rgb(180, 83, 9)
  4. Give inputs with a required attribute a 2px solid border of rgb(220, 38, 38)
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Top-level item is bold
  • Nested item is not bold
  • Paragraph after h2 is grey
  • Zip link is amber
  • Required input has a red border