Combinators and attribute selectors

Select elements by their relationship to other elements, or by the attributes they carry.

  • Beginner
  • 6 min read
  • Updated 17 Oct 2026

What you will learn

  • Select elements by relationship
  • Choose between descendant and child combinators
  • Use sibling combinators
  • Select by attribute

Type, class and ID selectors look at one element on its own. Combinators let you select an element based on where it sits in the HTML, and attribute selectors let you select by any attribute. This lesson is a reference you can come back to.

The four combinators

A combinator goes between two selectors and describes how the elements are related.

CombinatorSyntaxSelects
DescendantA BEvery B anywhere inside A
ChildA > BEvery B that is a direct child of A
Next siblingA + BThe B immediately after A, with the same parent
Subsequent siblingA ~ BEvery B after A, with the same parent

All the examples below use this HTML:

HTML
<article class="post">
    <h2>Recipe</h2>
    <p>Intro paragraph.</p>
    <p>Second paragraph.</p>
    <div class="note">
        <p>A paragraph inside a note.</p>
    </div>
    <p>Final paragraph.</p>
</article>

Descendant combinator: a space

CSS
.post p {
    color: #334e68;
}

Matches all four paragraphs, including the one inside .note. The space means "anywhere inside, at any depth".

This is the combinator you will use most. Keep descendant chains short: .post p is fine, but body main .content .post div p is fragile and very specific. Any change to the HTML structure breaks it.

Child combinator: >

CSS
.post > p {
    margin-bottom: 1rem;
}

Matches the three paragraphs that are direct children of .post, but not the one inside .note, which is a grandchild. Use > when nested components have elements of the same type and you do not want styles to leak into them.

CSS
.menu > li {
    display: inline-block;   /* only the top level, not nested sub-menus */
}

Next-sibling combinator: +

CSS
h2 + p {
    font-size: 1.125rem;
}

Matches only the "Intro paragraph", the first <p> straight after the <h2>. Both must share a parent. A common use is styling the lead paragraph after a heading, or adding space between consecutive items:

CSS
.field + .field {
    margin-top: 1rem;   /* every field except the first */
}

Subsequent-sibling combinator: ~

CSS
h2 ~ p {
    color: #486581;
}

Matches "Intro paragraph", "Second paragraph" and "Final paragraph": every <p> sibling that comes after the <h2>. The paragraph inside .note is not a sibling, so it does not match.

Sibling combinators only look forwards. CSS selectors traditionally could not select an element based on what comes after it, or a parent based on its children. The modern :has() selector removes that limit. It has its own lesson later in the course.

Combinators and specificity

Combinators add nothing to specificity; only the simple selectors count. .post > p and .post p both score (0, 1, 1). The choice between them is about which elements match, not which wins.

Attribute selectors

Attribute selectors match elements by their attributes, written in square brackets.

SelectorMatches when the attribute
[attr]Exists, with any value
[attr="value"]Equals the value exactly
[attr~="value"]Contains the value as a whole space-separated word
[attr|="value"]Equals the value or starts with value followed by a hyphen
[attr^="value"]Starts with the value
[attr$="value"]Ends with the value
[attr*="value"]Contains the value anywhere

Add a space and i before the closing bracket to match case-insensitively: [href$=".pdf" i] also matches .PDF.

Practical examples

Style form inputs by type:

CSS
input[type="email"],
input[type="password"] {
    width: 100%;
    max-width: 24rem;
}

input[type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
}

Mark links to external sites and PDF files:

CSS
a[href^="http"] {
    text-decoration-style: dotted;
}

a[href$=".pdf" i] {
    font-weight: 600;
}

Highlight elements by language:

CSS
[lang|="fr"] {
    font-style: italic;   /* matches lang="fr" and lang="fr-CA" */
}

Styling with state attributes

ARIA and HTML state attributes make reliable hooks, because the styling stays in sync with the state that assistive technology reads:

HTML
<button class="menu-toggle" aria-expanded="false">Menu</button>
CSS
.menu-toggle[aria-expanded="true"] {
    background-color: #102a43;
    color: white;
}

[hidden] {
    display: none !important;
}

When JavaScript changes aria-expanded to "true", the style updates automatically. You do not need a separate .is-open class that could drift out of sync. The [hidden] rule is one of the rare fair uses of !important: it makes sure the hidden attribute always hides an element, even if a class sets display: flex.

Attribute selectors have the same specificity as a class, (0, 1, 0). input[type="email"] scores (0, 1, 1).

Data attributes

Custom data-* attributes are another useful hook, especially for variants:

HTML
<span class="badge" data-status="paid">Paid</span>
<span class="badge" data-status="overdue">Overdue</span>
CSS
.badge[data-status="paid"] {
    background-color: #e3f9e5;
    color: #05400a;
}

.badge[data-status="overdue"] {
    background-color: #ffe3e3;
    color: #610404;
}

Combining everything

Selectors can be combined freely:

CSS
.post > h2 + p,
.form input[type="text"] ~ .hint {
    color: #486581;
}

Readability suffers quickly, though. If a selector needs more than two or three parts, it is often clearer to add a class to the element in the HTML.

Debugging relationship selectors

  1. Inspect the element you expected to match. Look at the breadcrumb trail at the bottom of the Elements panel to see its real ancestors.
  2. Check whether there is an extra wrapper element. A > selector fails as soon as a <div> sits between parent and child.
  3. For +, check for elements in between, including ones you cannot see, such as a hidden <input>.
  4. In the Elements panel, press Ctrl+F (Cmd+F on macOS) and type a CSS selector. Chrome and Edge highlight every element it matches.

Summary

  • A space selects descendants at any depth; > selects direct children only.
  • + selects the next sibling; ~ selects all following siblings with the same parent.
  • Combinators do not add specificity; keep chains short to avoid fragile CSS.
  • Attribute selectors match existence, exact values, prefixes, suffixes and substrings.
  • State attributes such as aria-expanded and data-* attributes make reliable styling hooks.

Further reading