We are still cooking the magic in the way!
Combinator Selectors: Descendant, Child, Sibling
What Are Combinator Selectors?
In CSS, a combinator is a character or symbol that defines the relationship between two selectors. While simple selectors (like .class, #id, or element) target elements directly, combinators let you target elements based on their position relative to other elements in the HTML document tree. This is one of the most powerful features of CSS because it allows you to style elements based on their context -- where they sit in the DOM hierarchy -- without adding extra classes to your markup.
CSS provides four combinators, each representing a different type of relationship:
- Descendant selector (space) -- selects elements nested at any depth inside an ancestor.
- Child selector (
>) -- selects only direct children of a parent. - Adjacent sibling selector (
+) -- selects the element immediately following a sibling. - General sibling selector (
~) -- selects all following siblings.
Understanding these combinators is essential for writing efficient, maintainable CSS. They reduce the need for excessive class names and let you leverage the natural structure of your HTML to apply styles precisely where needed.
nav a, the nav is the context and a is the target being styled.The Descendant Selector (Space)
The descendant selector is represented by a single space between two selectors. It matches elements that are nested inside the first selector at any depth -- whether they are direct children, grandchildren, great-grandchildren, or even deeper. This is the most commonly used combinator in CSS and the one most developers learn first.
The general syntax is:
Descendant Selector Syntax
ancestor descendant {
property: value;
}
/* This selects ALL <a> elements inside <nav>, no matter how deep */
nav a {
color: white;
text-decoration: none;
}
Consider this HTML structure:
HTML Structure Example
<nav class="main-nav">
<a href="/">Home</a>
<div class="dropdown">
<a href="/about">About</a>
<ul class="submenu">
<li>
<a href="/team">Team</a>
</li>
<li>
<a href="/history">History</a>
</li>
</ul>
</div>
</nav>
The selector .main-nav a will match all four anchor elements: "Home" (direct child), "About" (grandchild via the dropdown div), and "Team" and "History" (deeply nested inside the submenu). The descendant selector does not care how many levels of nesting exist between the ancestor and the target.
Practical Examples of the Descendant Selector
Example: Styling Nested Navigation Links
/* All links inside the header */
header a {
color: #ecf0f1;
font-weight: 500;
transition: color 0.3s ease;
}
header a:hover {
color: #3498db;
}
/* All paragraphs inside an article */
article p {
line-height: 1.8;
margin-bottom: 1.2em;
color: #333;
}
/* All list items inside the sidebar */
.sidebar ul li {
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
/* All images inside a figure within an article */
article figure img {
width: 100%;
height: auto;
border-radius: 8px;
}
Multiple Levels of Descendant Selection
You can chain multiple descendant selectors to drill deeper into nested structures. However, each additional level adds specificity and reduces performance, so use this with care.
Example: Multi-Level Descendant Selectors
/* Paragraphs inside a card body inside a card inside the main content */
.main-content .card .card-body p {
font-size: 0.95rem;
color: #555;
}
/* Links inside list items inside an unordered list inside the footer */
footer ul li a {
color: #bdc3c7;
text-decoration: none;
}
footer ul li a:hover {
color: #fff;
text-decoration: underline;
}
body div.wrapper main section article div.content p span.highlight. This is fragile (any HTML restructuring breaks it), overly specific (hard to override), and slower for the browser to match. Aim for a maximum of 3 levels of nesting in most cases. If you need deeper targeting, consider adding a descriptive class to the element instead.Performance Implications of Descendant Selectors
Browsers evaluate CSS selectors from right to left. When the browser encounters nav a, it first finds every single <a> element on the page, then checks each one to see if it has a <nav> ancestor. On pages with hundreds of links, this can be less efficient than using a class like .nav-link. In most cases, the performance difference is negligible, but it becomes important in performance-critical applications or pages with very large DOMs.
.nav-link is significantly faster than header nav ul li a because the browser only needs to check one condition rather than traversing multiple ancestors.The Child Selector (>)
The child selector, written as >, selects only the direct children of an element. Unlike the descendant selector, it does not reach into deeper levels of nesting. The target element must be an immediate child of the parent -- no grandchildren, no deeply nested elements.
Child Selector Syntax
parent > child {
property: value;
}
/* Only direct <li> children of .menu, NOT nested sub-menu items */
.menu > li {
display: inline-block;
padding: 10px 20px;
}
This distinction between descendant and child selectors is crucial. Consider this HTML:
Understanding the Difference
<ul class="menu">
<li>Home</li> <!-- Direct child: SELECTED by .menu > li -->
<li>Products
<ul class="submenu">
<li>Laptops</li> <!-- NOT a direct child: NOT selected -->
<li>Phones</li> <!-- NOT a direct child: NOT selected -->
</ul>
</li>
<li>Contact</li> <!-- Direct child: SELECTED by .menu > li -->
</ul>
/* CSS */
/* Descendant: selects ALL li elements (5 total) */
.menu li {
color: blue;
}
/* Child: selects only the 3 direct children */
.menu > li {
color: red;
font-weight: bold;
}
Practical Examples of the Child Selector
Example: Styling Navigation with Nested Dropdowns
/* Style only top-level navigation links, not dropdown links */
.navbar > a {
font-size: 1rem;
font-weight: 600;
padding: 12px 16px;
color: #2c3e50;
text-decoration: none;
}
/* Dropdown links get different styling */
.dropdown-menu > a {
font-size: 0.9rem;
font-weight: 400;
padding: 8px 16px;
color: #555;
display: block;
}
/* Only direct children of the grid container get grid styling */
.grid-container > div {
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
/* Remove margin from the last direct child paragraph only */
.card-body > p:last-child {
margin-bottom: 0;
}
Example: Multi-Level Navigation Menu
/* Top-level menu items are horizontal */
.nav-menu > li {
display: inline-block;
position: relative;
}
/* Second-level items are vertical (dropdown) */
.nav-menu > li > ul > li {
display: block;
width: 200px;
}
/* Third-level items (flyout menus) */
.nav-menu > li > ul > li > ul > li {
display: block;
width: 180px;
}
/* Direct child links of top-level items */
.nav-menu > li > a {
padding: 15px 20px;
font-weight: 600;
text-transform: uppercase;
}
/* Direct child links of dropdown items */
.nav-menu > li > ul > li > a {
padding: 10px 16px;
font-weight: 400;
font-size: 0.9rem;
}
.card > .card-header ensures you only style the card's own header, not a header inside a nested card.Using the Child Selector with the Universal Selector
Combining the child selector with the universal selector (*) lets you target all direct children of an element regardless of their type. This is a powerful pattern for layout and spacing.
Example: Spacing All Direct Children
/* Add vertical spacing between all direct children of a section */
.content-section > * {
margin-bottom: 1.5rem;
}
/* Remove the margin from the last direct child */
.content-section > *:last-child {
margin-bottom: 0;
}
/* The "lobotomized owl" selector -- space between adjacent siblings */
.stack > * + * {
margin-top: 1.5rem;
}
/* Reset all direct children to have no padding */
.reset-container > * {
padding: 0;
margin: 0;
}
The Adjacent Sibling Selector (+)
The adjacent sibling selector, written as +, selects an element that is the immediately following sibling of another element. Both elements must share the same parent, and the target must come directly after the reference element with no other elements in between.
Adjacent Sibling Selector Syntax
element1 + element2 {
property: value;
}
/* Select the paragraph that comes immediately after an h2 */
h2 + p {
font-size: 1.1rem;
color: #555;
margin-top: 0.5rem;
}
Consider this HTML:
Understanding Adjacent Siblings
<article>
<h2>Introduction</h2>
<p>This paragraph IS selected by h2 + p</p>
<p>This paragraph is NOT selected (not immediately after h2)</p>
<h2>Details</h2>
<p>This paragraph IS selected (immediately after an h2)</p>
<ul>
<li>Item</li>
</ul>
<p>This paragraph is NOT selected (preceded by ul, not h2)</p>
</article>
Practical Examples of the Adjacent Sibling Selector
Example: Typography and Content Flow
/* Style the first paragraph after every heading */
h1 + p,
h2 + p,
h3 + p {
font-size: 1.1em;
color: #666;
line-height: 1.9;
}
/* Reduce the top margin when a heading follows another heading */
h2 + h3 {
margin-top: 0.5rem;
}
/* Add extra spacing after images */
img + p {
margin-top: 2rem;
}
/* Style a caption that comes right after a figure */
figure + figcaption {
font-style: italic;
color: #888;
font-size: 0.9em;
margin-top: 0.5rem;
}
Example: Form Label and Input Pairs
/* Style input fields that come right after labels */
label + input,
label + select,
label + textarea {
margin-top: 4px;
display: block;
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
}
/* Style error messages that appear right after invalid inputs */
input.error + .error-message {
display: block;
color: #e74c3c;
font-size: 0.85rem;
margin-top: 4px;
}
/* Style hint text after an input */
input + .hint {
color: #95a5a6;
font-size: 0.8rem;
margin-top: 2px;
}
Example: The Lobotomized Owl Selector
/* One of the most famous uses of the adjacent sibling selector.
This adds top margin to every element that follows another element.
The result: even spacing between siblings without margin on the first child. */
.flow > * + * {
margin-top: 1.5em;
}
/* This is equivalent to writing:
.flow > *:not(:first-child) { margin-top: 1.5em; }
but the "owl" selector (named for its appearance: * + *) is
a well-known CSS pattern. */
/* Variation: different spacing for different element types */
.article-content > * + * {
margin-top: 1rem;
}
.article-content > * + h2 {
margin-top: 3rem;
}
.article-content > * + h3 {
margin-top: 2rem;
}
A + B means "B that is immediately preceded by A," not "A that is immediately followed by B." The styling is always applied to the element on the right side of the +.The General Sibling Selector (~)
The general sibling selector, written as ~, selects all siblings that come after the reference element. Unlike the adjacent sibling selector (+), the target elements do not need to be immediately following -- they can have any number of other elements in between, as long as they share the same parent and come after the reference element in the source order.
General Sibling Selector Syntax
element1 ~ element2 {
property: value;
}
/* Select ALL paragraphs that come after an h2, not just the first one */
h2 ~ p {
color: #333;
line-height: 1.8;
}
Let us compare the adjacent and general sibling selectors:
Adjacent vs. General Sibling Selectors
<div class="container">
<h2>Title</h2>
<p>Paragraph 1</p> <!-- h2 + p: YES | h2 ~ p: YES -->
<span>A span</span>
<p>Paragraph 2</p> <!-- h2 + p: NO | h2 ~ p: YES -->
<div>A div</div>
<p>Paragraph 3</p> <!-- h2 + p: NO | h2 ~ p: YES -->
</div>
/* CSS */
/* Adjacent: only selects Paragraph 1 */
h2 + p {
font-weight: bold;
}
/* General: selects Paragraphs 1, 2, and 3 */
h2 ~ p {
color: #2c3e50;
}
Practical Examples of the General Sibling Selector
Example: Content Section Styling
/* Style all paragraphs that follow a heading in the article */
.article-body h2 ~ p {
padding-left: 1rem;
border-left: 3px solid #3498db;
}
/* All list items after the "active" item get faded out */
.step-list li.active ~ li {
opacity: 0.5;
color: #95a5a6;
}
/* All cards after the featured card get smaller */
.card-grid .featured ~ .card {
transform: scale(0.95);
}
Example: CSS-Only Interactive Patterns
/* CSS-only accordion using checkbox hack + general sibling */
.accordion-toggle {
display: none;
}
.accordion-toggle:checked ~ .accordion-content {
max-height: 500px;
opacity: 1;
padding: 16px;
}
.accordion-toggle:checked ~ .accordion-label::after {
transform: rotate(180deg);
}
/* CSS-only mobile navigation toggle */
#nav-toggle:checked ~ .nav-menu {
display: flex;
transform: translateX(0);
}
#nav-toggle:checked ~ .nav-overlay {
display: block;
opacity: 1;
}
/* Star rating system */
.star-rating input:checked ~ label {
color: #f39c12;
}
:checked state combined with ~ to toggle visibility of sibling elements -- is a classic technique for creating accordions, tabs, toggles, and mobile menus without JavaScript.Combining Combinators for Complex Selections
The real power of combinators emerges when you combine them. You can chain different combinators to create highly specific and contextual selectors. Each combinator in the chain adds another layer of relationship specificity.
Example: Combining Multiple Combinators
/* Direct children of nav that are <li> elements,
then select anchor links inside them (at any depth) */
.main-nav > li a {
color: #2c3e50;
text-decoration: none;
}
/* The paragraph immediately after the first direct child heading of article */
article > h2:first-of-type + p {
font-size: 1.15em;
font-weight: 500;
color: #34495e;
}
/* All paragraphs following an image inside a direct child figure of article */
article > figure img ~ p {
font-style: italic;
font-size: 0.9em;
}
/* Direct child list items of .sidebar nav ul */
.sidebar > nav > ul > li {
padding: 12px 0;
border-bottom: 1px solid #eee;
}
/* The element after an active item in the direct children of a steps list */
.steps > .active + .step {
border-left-color: #3498db;
}
Example: Card Layout with Complex Selectors
/* Card grid layout */
.card-grid > .card {
padding: 24px;
border: 1px solid #e0e0e0;
border-radius: 12px;
}
/* The card image followed by the card body */
.card > .card-image + .card-body {
padding-top: 16px;
}
/* All paragraphs after the first paragraph in card body */
.card > .card-body > p ~ p {
margin-top: 0.75rem;
color: #666;
}
/* The action bar that is a direct child of the card, after the body */
.card > .card-body ~ .card-actions {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid #eee;
}
/* Links inside the card actions area */
.card > .card-actions > a {
font-weight: 600;
color: #3498db;
}
Real-World Use Cases
Use Case 1: Blog Post Typography
A complete typographic system for blog content using combinators to control spacing and visual hierarchy:
Example: Blog Typography System
.blog-post > * + * {
margin-top: 1.5rem;
}
/* Headings need more space above them */
.blog-post > * + h2 {
margin-top: 3rem;
}
.blog-post > * + h3 {
margin-top: 2.5rem;
}
/* Less space when a subheading follows a heading */
.blog-post > h2 + h3 {
margin-top: 1rem;
}
/* The lead paragraph after the title */
.blog-post > h1 + p {
font-size: 1.25rem;
color: #555;
line-height: 1.9;
}
/* Blockquotes after paragraphs get extra spacing */
.blog-post > p + blockquote {
margin-top: 2rem;
margin-bottom: 2rem;
}
/* Code blocks after paragraphs */
.blog-post > p + pre {
margin-top: 1.5rem;
}
/* List items that follow other list items */
.blog-post li + li {
margin-top: 0.5rem;
}
Use Case 2: Form Layout
Example: Form Styling with Combinators
/* Space between form groups */
.form > .form-group + .form-group {
margin-top: 1.5rem;
}
/* Labels that are direct children of form groups */
.form-group > label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: #2c3e50;
}
/* Input/select/textarea directly after a label */
.form-group > label + input,
.form-group > label + select,
.form-group > label + textarea {
width: 100%;
padding: 10px 14px;
border: 2px solid #ddd;
border-radius: 8px;
transition: border-color 0.3s ease;
}
/* Help text after any input element */
.form-group > input ~ .help-text,
.form-group > select ~ .help-text {
font-size: 0.85rem;
color: #888;
margin-top: 4px;
}
/* Error message immediately after an input */
.form-group > input + .error-msg {
color: #e74c3c;
font-size: 0.85rem;
margin-top: 4px;
}
/* Submit button section after all form groups */
.form > .form-group ~ .form-actions {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #eee;
}
Use Case 3: Sidebar Navigation
Example: Sidebar with Nested Navigation
/* Top-level sidebar sections */
.sidebar > .nav-section + .nav-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #eee;
}
/* Section titles */
.nav-section > h3 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #999;
margin-bottom: 0.75rem;
}
/* Direct nav links */
.nav-section > ul > li > a {
display: block;
padding: 8px 12px;
color: #333;
border-radius: 6px;
transition: all 0.2s ease;
}
.nav-section > ul > li > a:hover {
background-color: #f0f0f0;
color: #3498db;
}
/* Nested sub-navigation links are indented and smaller */
.nav-section > ul > li > ul > li > a {
padding-left: 32px;
font-size: 0.9rem;
color: #666;
}
/* Active state and its following siblings */
.nav-section li.active > a {
background-color: #ebf5fb;
color: #3498db;
font-weight: 600;
}
.nav-section li.active ~ li > a {
opacity: 0.7;
}
Combinator Comparison Table
Here is a quick reference comparing all four combinators side by side:
All Four Combinators at a Glance
/* DESCENDANT (space) -- any depth */
.parent .target { }
/* Selects: any .target inside .parent, no matter how deep */
/* CHILD (>) -- direct children only */
.parent > .target { }
/* Selects: .target elements that are direct children of .parent */
/* ADJACENT SIBLING (+) -- immediately next */
.reference + .target { }
/* Selects: the .target immediately after .reference (same parent) */
/* GENERAL SIBLING (~) -- all following */
.reference ~ .target { }
/* Selects: all .target elements after .reference (same parent) */
Common Mistakes and Debugging Tips
Mistake 1: Confusing Descendant and Child Selectors
Descendant vs. Child Confusion
/* PROBLEM: You want to style only top-level items, but you
used the descendant selector which also styles nested items */
.menu li {
font-weight: bold; /* This affects ALL li, including nested ones */
}
/* SOLUTION: Use the child selector */
.menu > li {
font-weight: bold; /* Only direct children */
}
/* ALTERNATIVE: Reset nested items */
.menu li {
font-weight: bold;
}
.menu li li {
font-weight: normal; /* Reset for nested */
}
Mistake 2: Expecting Backward Selection
Sibling Selectors Only Work Forward
/* PROBLEM: You want to style the element BEFORE the active one */
/* This does NOT work -- CSS cannot select previous siblings */
.item + .active {
/* This selects .active, not .item! */
}
/* There is no "previous sibling" selector in CSS.
The + and ~ combinators only look FORWARD.
Workaround options:
1. Use JavaScript to add a class to the previous element
2. Restructure your HTML with flexbox order or direction
3. Use :has() (modern browsers) */
/* Modern solution with :has() */
.item:has(+ .active) {
/* This selects .item that is immediately followed by .active */
border-right: none;
}
Mistake 3: Forgetting That Siblings Must Share the Same Parent
Same Parent Requirement
/* HTML */
<div class="wrapper">
<h2>Title</h2>
</div>
<p>This paragraph is NOT a sibling of the h2!</p>
/* This will NOT work because p and h2 have different parents */
h2 + p {
/* Nothing selected -- they are not siblings */
}
/* To make this work, they must share the same parent:
<div class="wrapper">
<h2>Title</h2>
<p>Now this IS a sibling of h2</p>
</div>
*/
Mistake 4: Over-Specificity with Chained Combinators
Avoiding Overly Specific Selectors
/* BAD: Too specific -- hard to override and fragile */
body > main > section.content > article > div.body > p + p {
margin-top: 1rem;
}
/* GOOD: Just specific enough */
.article-body > p + p {
margin-top: 1rem;
}
/* BAD: Mixing too many combinators makes it unreadable */
.sidebar > nav ul li a ~ span + .icon > svg {
fill: currentColor;
}
/* GOOD: Use a descriptive class instead */
.nav-icon svg {
fill: currentColor;
}
Debugging Combinator Issues
When combinator selectors are not working as expected, here are steps to debug:
- Inspect the DOM structure: Open your browser's DevTools and verify the actual parent-child and sibling relationships. Elements might not be structured the way you expect.
- Check for text nodes: In the DOM, whitespace between elements creates text nodes. While these do not affect sibling selectors in CSS (CSS ignores text nodes for
+and~), they can confuse your mental model. - Verify the selector direction: Remember that the styled element is always the one on the right side of the combinator. If you wrote
A + B, it isBthat gets styled, notA. - Check element types: If you wrote
h2 + pbut there is a<div>between the<h2>and<p>, the adjacent sibling selector will not match. Useh2 ~ pif there might be other elements in between. - Test with a highlight: Add a temporary
outline: 3px solid red !important;to your selector to visually see which elements are being matched.
Exercise 1: Navigation Menu System
Build a two-level horizontal navigation menu with the following requirements:
- Create a
<nav>containing a<ul>with 5<li>items, each with an<a>link. - Two of the items should have nested
<ul>dropdown sub-menus with 3 items each. - Use the child selector to make only top-level
<li>items display inline. - Use the descendant selector to style all
<a>elements within the nav. - Use the child selector to give different styles to top-level links vs. dropdown links.
- Use the adjacent sibling selector to add a left border separator between top-level items.
- Use the general sibling selector to fade out items after the active one in the dropdown.
Exercise 2: Blog Post Layout
Create a blog post page and use combinator selectors to style the typography:
- Create an
<article>with an<h1>title, several<h2>sections, paragraphs, images, blockquotes, and code blocks. - Use
> * + *(child + adjacent sibling) on the article to add vertical rhythm spacing. - Use
> h1 + pto style the lead paragraph differently (larger font, lighter color). - Use
> * + h2to add extra top margin before each section heading. - Use
h2 + h3to reduce spacing when a subheading follows a heading. - Use
blockquote ~ pto add a subtle left border to paragraphs that follow blockquotes. - Use the descendant selector to style links inside paragraphs differently from navigation links.
Exercise 3: Pricing Table
Build a pricing comparison table using combinator selectors:
- Create 3 pricing cards side by side in a container.
- Use
>to ensure only direct child cards get grid/flex styling. - Use
.card + .cardto add a left border between adjacent cards. - Use
.featured ~ .cardto make all cards after the featured one slightly smaller. - Inside each card, use
> * + *for consistent vertical spacing. - Use
.price + .featuresto control spacing between the price and features list. - Use
.features > li + lito add dividers between feature items.
Summary
In this lesson, you learned about the four CSS combinator selectors and how they define relationships between elements:
- Descendant selector (space) selects elements nested at any depth. It is the most commonly used combinator but can be less efficient for very deep or broad selections.
- Child selector (>) selects only direct children. It is essential for component-based styling where you need precise control over which level of nesting gets styled.
- Adjacent sibling selector (+) selects the immediately following sibling. It is perfect for contextual styling like "the paragraph after a heading" or the "lobotomized owl" spacing pattern.
- General sibling selector (~) selects all following siblings. It powers CSS-only interactive patterns like the checkbox hack for accordions and toggles.
- Combinators can be chained together for complex, precise selections, but you should avoid excessive specificity.
- Browsers evaluate selectors right to left, so deep descendant selectors can have performance implications on large pages.
- Sibling selectors only work forward in the DOM -- there is no native CSS way to select a previous sibling (though
:has()offers a modern workaround). - When debugging, always verify the actual DOM structure and remember that the styled element is always the one on the right side of the combinator.