March 14, 2007
Adjacent Sibling Selectors in CSS
The adjacent sibling combinator lets CSS select an element based on whatever appears immediately before it. It is a small piece of syntax that can eliminate unnecessary classes, take advantage of the document structure, and make a stylesheet considerably easier to maintain.
In the article before this one, I wrote about strange CSS rules.
Some selectors are complex. Others are unusual. A few are genuinely beautiful, at least to the sort of person who can look at a line of CSS and feel something. We are a small but committed population. CSS is much more than changing the color of text and adding a hover effect to links. It is a language for applying rules to a document, and most of its real power comes from understanding how to select the correct elements.
Beginners normally start with type selectors:
p {
color: red;
}
That rule makes every paragraph red.
Soon afterward, they discover classes:
p.note {
color: #666;
}
Then come descendant selectors:
article p {
color: red;
}
This selects every paragraph contained anywhere inside an <article>, regardless of how deeply nested it is. It should not be confused with the child combinator:
article > p {
color: red;
}
That version selects only paragraphs that are direct children of the article. The difference is one character, which is the sort of efficiency CSS uses when preparing an afternoon of debugging. These basic selectors are enough to build a site. They are not always enough to build one cleanly.
When developers do not know how to express relationships between elements, they usually compensate by adding classes. Then they add more classes to override the first classes, followed by several identifiers, a few !important declarations, and a quiet understanding that nobody will touch the stylesheet before the next redesign. CSS gives us better tools.
One of them is the adjacent sibling combinator, written with a plus sign:
A + B {
/* styles */
}
It selects B when B appears immediately after A and both elements share the same parent. Simple syntax. Surprisingly useful.
Selecting the next paragraph
Consider this HTML:
<div>
<h1>This is a title</h1>
<p>The first paragraph of the document.</p>
<p>The second paragraph of the document.</p>
</div>
With this rule:
p {
color: red;
}
Both paragraphs become red. With this rule:
h1 + p {
color: red;
}
Only the first paragraph becomes red. The selector reads almost like a sentence:
Select a paragraph that appears immediately after an
h1.
The second paragraph is not selected because it follows another paragraph, not the heading. The word immediately matters.
This markup matches:
<h1>Title</h1>
<p>Selected paragraph.</p>
This markup does not:
<h1>Title</h1>
<h2>Subtitle</h2>
<p>Paragraph.</p>
The paragraph still appears after the h1, but it is not the next sibling. The h2 is standing in the way, doing exactly what we asked it to do and ruining the selector with complete innocence.
Both elements must also share the same parent.
This does not match:
<header>
<h1>Title</h1>
</header>
<p>Paragraph.</p>
The heading belongs to <header>, while the paragraph belongs to whatever contains the header. They may look adjacent on the page, but they are not siblings in the document tree. CSS is very strict about family relationships.
Styling every paragraph except the first
The adjacent sibling combinator becomes particularly useful with repeated elements.
Consider this rule:
p + p {
color: red;
}
And this markup:
<div>
<h1>This is a title</h1>
<p>The first paragraph.</p>
<p>The second paragraph.</p>
<p>The third paragraph.</p>
</div>
The second and third paragraphs become red. The first paragraph does not, because it follows an h1. The second follows a paragraph, and the third follows another paragraph.
This pattern is useful for spacing:
p + p {
margin-top: 1.25rem;
}
Instead of adding a bottom margin to every paragraph and then removing it from the last one, we add space only when one paragraph follows another. The rule describes the actual relationship we care about. No helper class. No special treatment for the first paragraph. No paragraph-but-not-the-first-one naming meeting. The document structure already contains the information. CSS merely needs to use it.
The same idea works with list items:
li + li {
border-top: 1px solid #ddd;
}
Every list item except the first receives a top border. Or navigation links:
nav a + a {
margin-left: 1rem;
}
Or form fields:
.field + .field {
margin-top: 1.5rem;
}
These selectors are small, readable, and based on structure rather than additional markup. That is usually a good trade.
Selecting a table column
In the original version of this article, I used an adjacent selector to target a particular table column. The idea was correct, but the counting was not.
This selector:
table.listings tr td:first-child + td + td + td {
background: #ffffcc;
}
selects the fourth cell in every row. Each + td moves one sibling forward:
td:first-child First cell
td:first-child + td Second cell
td:first-child + td + td Third cell
Therefore, to select the third cell, the rule should be:
table.listings tr td:first-child + td + td {
background: #ffffcc;
}
It works because the selector begins with the first cell and advances through two immediately adjacent cells.
In 2007, this was a useful way to avoid adding the same class to every generated cell:
<td class="highlighted-column">
When a server produced hundreds of rows, avoiding repeated markup could make the document smaller and keep presentation decisions out of the HTML.
Today, however, the clearer solution is usually :nth-child():
table.listings td:nth-child(3) {
background: #ffffcc;
}
It says exactly what we mean: select the third cell among its siblings. The adjacent-selector version is still valid. It is simply more ceremonial, like reaching the third floor by climbing through two balconies. This is an important lesson about CSS. A clever selector is not automatically the best selector. Use the one that communicates the intention most clearly.
Titles and subtitles
Another useful case involves headings. Suppose every article begins with a title, and some titles are followed by a subtitle:
<article>
<h1>Understanding the Cascade</h1>
<p class="subtitle">Why your rule lost another argument</p>
</article>
We can style the subtitle only when it appears directly after the title:
h1 + .subtitle {
margin-top: 0;
color: #666;
}
That part has always been possible. What we could not do in 2007 was select the h1 based on the subtitle that followed it. CSS selectors generally worked forward through the document. We could select the next sibling, but not travel backward and change the previous one.
Today, :has() makes this possible:
h1:has(+ .subtitle) {
margin-bottom: 0;
}
This means:
Select an
h1that has an adjacent.subtitle.
Without the subtitle, the heading keeps its normal margin:
h1 {
margin-bottom: 1.5rem;
}
h1:has(+ .subtitle) {
margin-bottom: 0;
}
In 2007, we needed a class on the heading, a class on the article, a server-side condition, or a different spacing strategy. Now CSS can inspect the relationship directly. It took a while, but CSS eventually learned to look over its shoulder.
Adjacent is not the same as subsequent
CSS also has a related combinator written with a tilde:
h1 ~ p {
color: red;
}
This is the subsequent-sibling combinator. It selects all matching siblings that appear after the first element, not only the immediately adjacent one.
Given this document:
<article>
<h1>Title</h1>
<h2>Subtitle</h2>
<p>First paragraph.</p>
<figure>Illustration.</figure>
<p>Second paragraph.</p>
</article>
This selector:
h1 + p {
color: red;
}
selects nothing because the next sibling is an h2.
This selector:
h1 ~ p {
color: red;
}
selects both paragraphs because they appear later, share the same parent, and match p.
The difference is easy to remember:
A + B The next B
A ~ B Every later B
Both combinators describe sibling relationships. One is very selective. The other is willing to meet the extended family.
Structure instead of extra classes
The main advantage of sibling selectors is not saving a few characters. It is expressing presentation through relationships that already exist in the document.
Consider a list of messages:
<div class="messages">
<article class="message">...</article>
<article class="message">...</article>
<article class="message">...</article>
</div>
We could add classes:
<article class="message first-message">...</article>
<article class="message following-message">...</article>
<article class="message following-message">...</article>
Or we could write:
.message + .message {
border-top: 1px solid #ddd;
}
The second version says that every message following another message should have a divider. That rule survives when messages are added, removed, or reordered. Nobody needs to calculate which item is first and modify its class. The HTML describes the content. CSS understands the relationship. This is where selectors become more than convenient syntax. They let us avoid copying presentational information into the document. Of course, structural selectors can be abused.
A selector such as this may technically work:
main article > section:first-child + section ul li + li a span {
color: red;
}
It may also cause the next developer to close the laptop and begin a new life cultivating olives. A selector should take advantage of structure without becoming dependent on every detail of that structure. When the relationship is meaningful and stable, use it. When the selector only works because you have memorized seventeen levels of markup, add a class and allow everyone to go home.
A note about specificity
Combinators do not add specificity by themselves.
The specificity of:
h1 + p
Comes from the two type selectors, h1 and p. The plus sign describes the relationship but does not make the rule stronger.
Similarly:
.message + .message
Has the specificity of two class selectors.
This matters because structural selectors can look more powerful than they are. A long selector is not necessarily difficult to override because of its length. Specificity depends on the selectors inside it, not on how dramatic the line appears in the editor. Still, do not use this as permission to write selectors that require horizontal scrolling. Readability remains a feature.
Browser compatibility in 2007
In 2007, the unpleasant part of this article was browser support. Internet Explorer 6 did not support adjacent sibling selectors. Internet Explorer 7 introduced support in standards mode, although its implementation had some historical quirks. Firefox, Safari, and Opera already handled the selector. That meant the feature was usable, but only when the design could tolerate a simpler result in IE6 or when the project had an alternative for that browser. This was normal web development at the time.
We designed for capable browsers, added fallbacks for older ones, and kept a virtual machine nearby for Internet Explorer. Occasionally we spoke to it using language not found in the CSS specification. Today, the adjacent sibling combinator is a standard, widely supported part of CSS. You can use it without designing an emergency response plan. The browser war has moved on to other territories.
Learn the tree
The adjacent sibling combinator is easy to learn:
A + B
Select B when it appears immediately after A. But the real lesson is larger. CSS becomes much easier when you stop seeing an HTML document as a flat pile of tags and begin seeing it as a tree of relationships. Elements have parents. They have children. They have siblings. Some siblings are adjacent. Others merely appear later. Selectors let us describe these relationships without changing the markup every time we want to alter the presentation. That is where CSS becomes interesting. It is not only a collection of visual properties. It is a language for asking questions about a document:
Is this paragraph inside an article?
Is this item the first child?
Does this field follow another field?
Is this heading followed by a subtitle?
Is this the third cell in the row?
The browser answers those questions continuously and applies the corresponding rules. No JavaScript. No extra classes. No server-side conditions. Just a plus sign, placed in the correct location. Simple as that.