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
| Combinator | Example | Matches |
|---|---|---|
| Descendant (space) | nav a | Every a anywhere inside a nav |
Child > | ul > li | li elements that are direct children of a ul |
Next sibling + | h2 + p | The first p that comes immediately after an h2 |
Subsequent sibling ~ | h2 ~ p | Every 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:
<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
| Selector | Matches 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
iinside 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.
Child vs descendant in a nested menu
Change > on line 1 to a space and run again. The nested item gets the border too.
<ul class="menu">
<li>Home</li>
<li>Shop
<ul>
<li>Shoes</li>
<li>Bags</li>
</ul>
</li>
<li>Contact</li>
</ul>.menu > li {
border-left: 4px solid #16a34a;
padding-left: 8px;
margin: 6px 0;
}
.menu {
list-style: none;
padding: 0;
font-family: sans-serif;
}Line-by-line explanation
- Line 1Only list items that are direct children of
.menuget the green bar. - Lines 2 to 4A green bar and spacing. The nested Shoes and Bags items keep the default look.
- Lines 7 to 11Remove bullets and padding from the outer list.
Sibling combinators for article spacing
<article>
<h2>Getting started</h2>
<p>This first paragraph after the heading is the lead.</p>
<p>Later paragraphs are normal text.</p>
<p>Another normal paragraph.</p>
</article>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;
}Line-by-line explanation
- Lines 3 to 7
h2 + pmatches only the paragraph straight after the heading, giving a larger lead paragraph. - Lines 9 to 11
h2 ~ pmatches every paragraph after the heading.
Styling links and inputs by attribute
<p><a href="https://example.com">External site</a></p>
<p><a href="/files/guide.PDF">Download the guide</a></p>
<p><a href="mailto:hello@example.com">Email us</a></p>
<form>
<input type="text" placeholder="Name">
<input type="email" placeholder="Email">
<input type="text" placeholder="Disabled" disabled>
</form>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; }Line-by-line explanation
- Line 1Links that start with
httpsget an arrow, marking them as external. - Line 2The
iflag matches.PDFas well as.pdf. - Line 3Email links get their own colour.
- Line 6Only the email input gets a blue border.
- Line 7Any input with the
disabledattribute looks inactive.
Common CSS Combinators Mistakes
Expecting + to look backwards
/* trying to style the h2 that comes before a p */
p + h2 { color: red; }/* 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
input [type="email"] { border-color: blue; }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
[class*="btn"] { padding: 10px; } /* also matches "submit-btn-wrapper" */.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.
Style only the list items that are direct children of .steps.
.steps li {
font-weight: bold;
}
> is the child combinator.Which selector matches every link whose address ends in .pdf?
$= means "ends with". ^= means "starts with".What does h3 ~ p select?
~ is the subsequent-sibling combinator.Which selector matches <input type="EMAIL"> and <input type="email">?
i flag makes the value match ignore case.Only the paragraph right after each heading should be bold, and only email inputs should have a blue border. Fix the selectors.
<h2>News</h2>
<p class="lead">First paragraph.</p>
<p class="body">Second paragraph.</p>
<input class="mail" type="email">
<input class="name" type="text">h2 ~ p { font-weight: bold; }
input [type=email] { border: 2px solid rgb(37, 99, 235); }- 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.
What does a space between two selectors mean, as in
footer a?- The elements are siblings
- Any
ainside afooter, at any depth - Only direct children
- Both selectors match the same element
Which combinator selects only direct children?
- ~
- >
- +
- space
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
Which selector matches links that start with
https?a[href^="https"]a[href$="https"]a[href~="https"]a:https
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
- Make every
lithat is a direct child of.tochavefont-weight: bold - Give the paragraph straight after each
h2the stylecolor: rgb(55, 65, 81) - Give links whose
hrefends in.zipacolorofrgb(180, 83, 9) - Give inputs with a
requiredattribute a2px solidborder ofrgb(220, 38, 38)
<ul class="toc">
<li class="top">Install
<ul><li class="sub">Windows</li></ul>
</li>
</ul>
<h2>Download</h2>
<p class="after">Pick the right file.</p>
<p class="later">Older versions are archived.</p>
<a class="zip" href="/files/app.zip">App</a>
<a class="page" href="/notes">Notes</a>
<input class="req" required>
<input class="opt">- 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