Element selection in CSS can sometimes be a bit more tricky than usual. Certain cases involve making complex logical selections, such as targeting a direct child, an immediate sibling, or any sibling to solve specific layout problems.
Fortunately, these complicated selections are made easy by a set of operators called CSS combinators. This article focuses on CSS sibling combinators, which — as their name suggests — select elements based on their position relative to each other in the source order.
Jump ahead:
By the end of the article, you’ll have a crystal-clear understanding of CSS sibling combinators, their usage, and their practical application in frontend web development. We’ll go through several examples to demonstrate these concepts, so you can check out the CodePen collection as we get started.
The sibling combinators enable you to select HTML elements located after a given HTML element in the source order. Note that two or more HTML elements are only said to be siblings if they share the same parent element.
There are two types of sibling selectors that share similar functionality but may have different use cases. Let’s look at them one by one with examples.
The adjacent sibling selector targets the element immediately next to a given HTML element. A plus sign +
is used to denote adjacent sibling selection in CSS:
.first + .next { /* <-- The logic for selection criteria */ /* Styles to be applied to the adjacent sibling */ }
Here’s how the adjacent sibling combinator would select a sibling element in CSS:
See the Pen Adjacent Sibling Combinator in CSS by Rahul C (@_rahul)
on CodePen.
Once you add the criteria logic for selection, the next step is to add suitable CSS style properties to the targeted element or elements. The example above shows the adjacent sibling selector in action, targeting the .next
class that follows immediately after the .first
one.
Note that only the .next
element immediate to .first
got selected, while its other occurrences got ignored. This behavior demonstrates the exclusive nature of the adjacent sibling combinator as far as element selection is concerned.
The general sibling selector in CSS enables you to select any sibling element that meets the specified criteria. It comes in handy when applying styles to multiple elements that follow a particular HTML element, regardless of their immediate order.
A tilde sign ~
denotes general sibling selection logic in CSS. Consider the following example to observe the contrast between this one and the previously-discussed adjacent sibling combinator:
.first ~ .next { /* Styles to be applied to general siblings */ }
Compare the result below to the adjacent sibling combinator example we saw previously:
See the Pen General Sibling Combinator in CSS (~) by Rahul C (@_rahul)
on CodePen.
The example above clearly shows the selection of any sibling that occurs after a given HTML element which exposes the inclusive selection behavior of the general sibling combinator.
The only fundamental difference between these two combinators lies in their selection approach, which eventually determines their functionality and applications.
The adjacent sibling combinator is exclusive in making its selections; it strictly looks for the immediate neighboring element to match specific criteria.
On the other hand, the general sibling combinator is more inclusive; it selects every sibling that meets a given criterion.
The adjacent sibling combinator has a wider range of practical applications than the general sibling combinator. Each segment ahead showcases some of these use cases and demonstrates how they work in real scenarios.
I will also attempt to include the general sibling combinator in some instances whenever possible.
Also, note that there can be alternative ways in modern CSS to carry out some of the below-mentioned tasks. Our strategy here is to explore and utilize the sibling selectors and evaluate their applicability in our projects.
While typesetting and spacing, there may be situations where we don’t need the bottom margin for the last element. Thanks to modern CSS pseudo-classes such as :not
and :last-child
, it’s pretty simple to disregard the margin or any given property for the last child of any given type:
p:not(:last-child) { margin-bottom: 1.5em; }
However, if you attempt to apply a utility class that has a different bottom margin to any of the elements styled using the above-mentioned CSS, it won’t function as intended.
This is because the two pseudo-classes have higher specificity than a plain old CSS utility class. Here’s an example for you to observe that issue closely:
See the Pen The specificity problem w/ :not(:last-child) by Rahul C (@_rahul)
on CodePen.
To address this issue, we can cut down on specificity with the help of the CSS adjacent sibling combinator. With this approach, we can easily apply utility classes with margin variations:
See the Pen Preventing :not(:last-child) from snipping last paragraph margin by Rahul C (@_rahul)
on CodePen.
The same technique can be used to adjust inline spacing as well. An ideal use case for this tweak can be a navigation menu where we want to trim or ignore the right margin for the last list item:
.inline-nav li + li { margin-left: 1em; }
See an example below:
See the Pen Preventing :not(:last-child) from snipping last list-item margin by Rahul C (@_rahul)
on CodePen.
Consider a scenario where we have some site header variations with and without the navigation element. It’s easy to use Flexbox to achieve a quick prototype for such a component by justifying the contents evenly to have the same spacing between them. For example, given the below HTML:
<header class="header"> <div class="logo">Logo</div> <form class="search"><input type="search" placeholder="Search..."></form> ... </header><!-- .header -->
Here’s what our CSS could look like:
.header { display: flex; align-items: center; justify-content: space-between; gap: 1.5em; }
Now, another variation with the navigation menu sticking to the left and disregarding the Flexbox content justification can be achieved with the adjacent sibling combinator:
.nav + .search { margin-left: auto; }
See all three header layout variations in action below, achieved without writing separate classes or tweaking position and transformation-based properties:
See the Pen Flexible Header navigation with CSS Sibling Combinators by Rahul C (@_rahul)
on CodePen.
Try the CodePen demo in fullscreen mode for a better viewing experience. Consider making these menus responsive as an assignment following the mobile-first responsive menu approach.
You can make your designs more interactive and user-friendly by adding event-based toggling of element display states. The adjacent sibling combinator can be highly useful when the element to be toggled is a sibling of the target.
A simple example can be displaying a ghost element that lies outside of the target when hovering or focusing on a given target. Usually the ::after
and ::before
pseudo-elements are used to solve such problems.
If you need to toggle the styles of an external sibling element based on the events on a target element, this technique fulfills that purpose:
button + p { display: none; } button:hover + p { display: block; }
Hover over the element in the CodePen below to see how the hover event toggles the ghost element’s display:
See the Pen Ghost element toggline w/ CSS Sibling Combinator by Rahul C (@_rahul)
on CodePen.
Display toggling is a commonly implemented feature in the frontend. Use this technique to experiment with the visibility, opacity, or other styles for a better variety of solutions for your layout problems.
We can use the infamous checkbox hack to emulate click-based toggling in our projects. It’s not recommended to use it professionally, but it is worth learning about — this is the approach people were using a few years back to minimize the use of JavaScript for small tasks.
The essence of this technique lies in setting a checkbox input as an event target, tracking its state with the :checked
pseudo-class, and then assigning styles to the checkbox’s siblings in conjunction with its states:
#toggle-sibling + p, #toggle-general-sibling ~ p { display: none; } #toggle-sibling:checked + p, #toggle-general-sibling:checked ~ p { display: block; }
Interact with the demo below to understand how this approach would look in action:
See the Pen Toggling display w/ Sibling and General Sibling Selectors by Rahul C (@_rahul)
on CodePen.
View the CodePen demo in fullscreen mode and try checking the checkbox inputs on and off to notice the difference. By altering the appearance of the event target — i.e., the checkbox input — we can greatly enhance the aesthetic of such CSS-only solutions.
Extending the above use case further, let’s create responsive navigation for a traditional site header area by following the mobile-first approach.
Creating a mobile-friendly navigation menu without relying on JavaScript is a bit of a tricky job. Our workaround uses CSS general sibling combinator and the checkbox hack to handle a navigation menu with some messy markup:
<header class="header"> <div class="logo">Logo</div> <!-- Checkbox hack / Toggle button for the menu --> <input class="nav-button" type="checkbox" id="menu"> <!-- Layout mess / additional elements around the event target --> <div class="stuff-that-sticks-to-logo">...</div> <nav class="nav"> <ul> <li>...</li> <li>...</li> </ul> </nav><!-- .nav --> </header><!-- .header -->
I presume that by this point, you understand that if there are no additional elements around the event target in the layout — i.e., the checkbox input — then using the adjacent sibling combinator will be sufficient.
The checkbox hack here is required to emulate click-based element toggling with CSS in mobile view. As discussed earlier, with hints of the checkbox states from the :checked
pseudo-class, we can easily control the display of our menu:
/* Keep the menu hidden by default */ .nav-button ~ .nav { display: none; } /* Change the display based on the checked state */ .nav-button:checked ~ .nav { display: block; }
See the result below:
See the Pen Responsive Navigation w/ CSS Sibling Combinator & Checkbox hack by Rahul C (@_rahul)
on CodePen.
Scale the above demo to 0.5x to quickly see its desktop variation, or view the full desktop version on CodePen.
Using this for production is not advisable, as there are more effective ways to design and implement a responsive navigation feature. However, this may be helpful for making quick fixes to older web layouts without completely redesigning them.
Consider improving this further by separating the menu component from the checkbox-hack logic for the larger screen.
Another useful application of the adjacent sibling can be dynamic form states without involving JavaScript. This one relies on the utility provided by :valid
and :invalid
pseudo-classes, which can be used to determine the states of any given input field.
To identify empty input fields, we can employ the :not
pseudo-class with :placeholder-shown
as its argument. The trick lies in assigning a placeholder to the input field, and when the user inputs text into the field, the :placeholder-shown
pseudo-class switches to false.
This logic can be used to exclude the blank inputs and apply the valid
and invalid
styles only on user input:
.email-field:not(:placeholder-shown):invalid + .submit-btn { background-color: red; border-color: red; color: white; } .email-field:not(:placeholder-shown):valid + .submit-btn { background-color: green; border-color: green; color: white; }
In the demo below, type an email in the input field and see how the styles switch dynamically whenever a right or wrong value is provided:
See the Pen Form valid/Invalid States w/ CSS Sibling Combinators by Rahul C (@_rahul)
on CodePen.
In this article, we learned about the sibling combinators in CSS, how they are used, and some of their key applications for solving different layout problems.
There may be other use cases that can vary depending on the context of the problems. I invite you to mention additional examples in the comments if I have missed any.
I really hope you learned something new from this tutorial guide. Check out the complete CodePen collection for this article here.
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.