What the CSS sibling selectors are

CSS has two sibling combinators that are basically “style this thing based on the thing right before it”. They only work when elements are:

  • Siblings (they share the same parent)
  • In a specific order in the HTML (because CSS normally reads left-to-right)

The two combinators are:

  • Adjacent sibling: A + B (B is the next sibling right after A)
  • General sibling: A ~ B (B is any sibling after A, not necessarily immediate)

The big mental model: Sibling selectors select elements that come after something. They do not go backwards… unless you bring in :has() (later we will).

/* These work only if the elements share the same parent */
.note + .note {
  border-top: 3px dashed #111;
}
  
/* This matches every .note after the first one */
.note ~ .note {
  opacity: 0.7;
}
  
/* This is NOT a sibling selector (just a normal descendant selector) */
.list .note {
  background: #fff6cf;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

body {
  font-family: ui-monospace, SFMono-Regular, Menlo;
}

.list {
  display: grid;
  gap: 12px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  max-width: 720px;
}

.note {
  padding: 14px 16px;
  border: 2px solid #111;
  background: white;
  border-radius: 12px;
}

.note strong {
  display: block;
  margin-bottom: 6px;
}
  
Note 1 First sibling note.
Note 2 Immediately after note 1.
Note 3 Also after note 1 (and note 2).

The adjacent sibling selector: A + B

A + B selects B only when it comes immediately after A, and both share the same parent.

  • It matches one element at most (the next sibling).
  • If there’s anything in between (another element), the match fails.
  • If B is wrapped in another element, it’s not a sibling anymore (so it fails).
/* Style only the paragraph that immediately follows an h3 */
h3 + p {
  margin-top: 6px;
  padding: 12px 14px;
  border-left: 6px solid #111;
  background: #e9f7ff;
}
  
/* This only styles a button that directly follows another button */
button + button {
  margin-left: 10px;
}
  
/* A classic: add spacing between list items without touching the first one */
li + li {
  margin-top: 10px;
}
  
/* If there is any element in between, this WON'T match */
h3 + .card {
  outline: 4px solid #111;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrapper {
  max-width: 780px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  font-family: ui-monospace, SFMono-Regular, Menlo;
}

h3 {
  margin: 0;
}

p {
  margin: 12px 0 0;
  line-height: 1.4;
}

.controls {
  margin-top: 18px;
}

button {
  border: 2px solid #111;
  background: white;
  padding: 10px 14px;
  border-radius: 10px;
  cursor: pointer;
}

ul {
  margin: 18px 0 0;
  padding-left: 18px;
}

.card {
  margin-top: 12px;
  padding: 12px 14px;
  border: 2px solid #111;
  border-radius: 12px;
  background: white;
}
  

Heading

I am immediately after the h3. The selector h3 + p can target me.

  • First item (no top margin)
  • Second item (margin via li + li)
  • Third item (also gets margin)
There is no selector active that outlines me by default.

CSS sibling selector after (adjacent)

When people say “CSS sibling selector after”, they usually mean: “Select the thing that comes after another thing.” That’s literally what + does.

A super common example: style a label after a checkbox, or style the next paragraph after a heading. The key is that the “after” element must be immediately after.

/* Checkbox + label: label is immediately after the input */
.toggle + label {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  border: 2px solid #111;
  border-radius: 999px;
  background: white;
  cursor: pointer;
}

/* When the checkbox is checked, style the label AFTER it */
.toggle:checked + label {
background: #d4ffd9;
}

.toggle:checked + label .pill {
transform: translateX(2px);
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-monospace, SFMono-Regular, Menlo;
  padding: 18px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #f3f3f3;
  max-width: 760px;
}

.row {
  display: flex;
  align-items: center;
  gap: 14px;
}

.toggle {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.pill {
  width: 44px;
  height: 22px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #fff;
  position: relative;
  display: inline-block;
}

.pill::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 999px;
  background: #111;
  transform: translateY(-50%);
  transition: transform 200ms ease;
}

.toggle:checked + label .pill::before {
  transform: translate(22px, -50%);
}
  

The general sibling selector: A ~ B

A ~ B selects every B that is a sibling of A and comes after it. Unlike +, it does not require adjacency.

  • B can be the next sibling… or the 10th sibling later.
  • A and B still must share the same parent.
  • It can match multiple elements (that’s the whole point).

This is great for “turn on all the things after this point” patterns.

/* Highlight ALL steps after the current step */
.step.is-current ~ .step {
  opacity: 0.55;
  filter: grayscale(1);
}

/* Highlight the current step itself */
.step.is-current {
border-color: #111;
background: #fff6cf;
} 
/* A classic: add top spacing to any paragraph after the first paragraph */
p ~ p {
  margin-top: 14px;
}
  
/* This targets any image AFTER a caption (if they share the same parent) */
.caption ~ img {
  outline: 4px solid #111;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  max-width: 760px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  border-radius: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo;
}

.steps {
  display: grid;
  gap: 10px;
  margin-bottom: 18px;
}

.step {
  border: 2px solid #111;
  border-color: #aaa;
  background: white;
  padding: 12px 14px;
  border-radius: 12px;
}

p {
  margin: 0;
  line-height: 1.45;
}

.caption {
  margin-top: 18px;
  display: inline-block;
  padding: 6px 10px;
  border: 2px solid #111;
  border-radius: 999px;
  background: white;
}

img {
  display: block;
  margin-top: 10px;
  width: 320px;
  height: 180px;
  object-fit: cover;
  border-radius: 12px;
  border: 2px solid #111;
}
  
Step 1: Start
Step 2: Current step
Step 3: After current
Step 4: Also after current

First paragraph.

Second paragraph (selected by p ~ p).

Third paragraph (also selected).

Caption Random scenic image

CSS sibling selector after (general)

If your goal is “style all matching siblings after this element”, ~ is the workhorse. Think “after, forever (until the parent ends)”.

~ matches following siblings that share the same parent, and does not require them to be immediately next.

CSS sibling selector with class

You can (and should) combine sibling combinators with classes. This is where sibling selectors stop being “CSS trivia” and become “I can style UI without extra wrappers”.

Here are a few very common patterns:

  • Icon + text: .icon + .text
  • Alert type: .alert--warning + .alert
  • Component spacing: .card + .card
/* Only add spacing when a card follows another card */
.card + .card {
  margin-top: 14px;
}

/* If a warning card is followed by ANY card, emphasize the next one too */
.card.is-warning + .card {
border-style: dashed;
} 
/* Add a “badge” look to any title that follows an icon */
.icon + .title {
  display: inline-flex;
  align-items: center;
  padding: 6px 10px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #fff6cf;
}
  
/* General sibling: if a card is “featured”, fade any cards after it */
.card.is-featured ~ .card {
  opacity: 0.75;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.panel {
  max-width: 760px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  border-radius: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo;
}

.row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}

.icon {
  width: 18px;
  height: 18px;
  border: 2px solid #111;
  border-radius: 4px;
  background: #111;
  display: inline-block;
}

.title {
  font-weight: 700;
}

.card {
  border: 2px solid #111;
  background: white;
  border-radius: 12px;
  padding: 12px 14px;
}

.card.is-warning {
  background: #ffe3e3;
}

.card.is-featured {
  background: #dff1ff;
}
  
Title after icon (selected by .icon + .title)
Normal card (first one)
Warning card
Card immediately after warning (selected by .card.is-warning + .card)
Card after featured (faded by ~)
Another card after featured (also faded)

CSS sibling selector before / previous (and why it’s tricky)

Here’s the annoying truth: CSS sibling combinators (+ and ~) only look forward. They select “the thing after”.

So if you want: “Style the element before this one” or “Select the previous sibling” you can’t do it with plain + or ~.

But you can do it with :has(), because it lets you select an element based on what it contains… or based on relative selectors like + something and ~ something.

Select the “previous sibling” with :has(+ …)

Think of it as: “Select an element if it has an adjacent sibling matching X.”

Example: .item:has(+ .item.is-selected) means: “Select .item if the very next sibling is .item.is-selected.”

/* The selected item */
.item.is-selected {
  background: #fff6cf;
  border-color: #111;
}

/* The item BEFORE the selected one (previous sibling) */
.item:has(+ .item.is-selected) {
border-style: dashed;
background: #e9f7ff;
}

/* The item AFTER the selected one (normal forward sibling selector) */
.item.is-selected + .item {
outline: 4px solid #111;
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.list {
  max-width: 760px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  border-radius: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo;
  display: grid;
  gap: 10px;
}

.item {
  border: 2px solid #aaa;
  background: white;
  border-radius: 12px;
  padding: 12px 14px;
}

.item strong {
  display: block;
  margin-bottom: 6px;
}
  
Item 1 Normal
Item 2 This becomes the “previous sibling” of Item 3
Item 3 Selected
Item 4 This is after the selected item

Select “any previous siblings” with :has(~ …)

If you want to style all siblings that come before something (not just the immediate one), you can use: :has(~ .target)

Example: .step:has(~ .step.is-current) means: “Select every .step that has a later sibling .step.is-current.”

/* Current step */
.step.is-current {
  background: #fff6cf;
  border-color: #111;
}

/* All steps BEFORE the current step */
.step:has(~ .step.is-current) {
opacity: 0.7;
filter: grayscale(1);
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.steps {
  max-width: 760px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  border-radius: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo;
  display: grid;
  gap: 10px;
}

.step {
  border: 2px solid #aaa;
  background: white;
  border-radius: 12px;
  padding: 12px 14px;
}
  
Step 1 (before current)
Step 2 (before current)
Step 3 (current)
Step 4 (after current)

Note: :has() is widely supported in modern browsers now, but it’s still worth checking compatibility for your audience.

Learn more about :has() in the CSS :has Pseudo-Class Interactive Tutorial.

CSS sibling selector not working (debugging checklist)

If + or ~ “does nothing”, it’s usually one of these:

1) The elements are not actually siblings

The #1 cause: there’s a wrapper in the way. Sibling selectors require the same parent.

  • h3 + p works: <h3>...</h3><p>...</p>
  • h3 + p fails: <h3>...</h3><div><p>...</p></div>

2) The element is not immediately after (for +)

+ is strict. If anything is in between, no match. If you need “any later sibling”, switch to ~.

3) You are targeting the wrong side of the selector

A + B selects B, not A. Same with A ~ B.

  • If you need to style A “because of B”, consider :has().

4) Specificity (or another rule) is overriding it

If you have something like .card p { ... } later in your stylesheet, it might override h3 + p.

  • Try temporarily adding a border/background so you can clearly see which rule wins.
  • Use more specific selectors if needed, but don’t go overboard.

5) DOM structure surprises

Sibling selectors are based on the DOM tree, not on how things look. CSS layout (Grid/Flex) doesn’t change sibling relationships.

Also, be careful with templating systems that insert extra wrappers or move elements around. If the HTML order changes, sibling selectors change too.

/* This works: .title is immediately followed by .msg */
.title + .msg {
  background: #dff1ff;
}

/* This fails: .title is NOT immediately followed by .msg */
.title + .msg.is-wrapped {
outline: 4px solid #111;
}

/* This works even with wrappers in between, but only if .msg is still a sibling */
.title ~ .msg {
border-left: 6px solid #111;
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  max-width: 760px;
  padding: 16px;
  border: 3px solid #111;
  background: #f3f3f3;
  border-radius: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo;
  display: grid;
  gap: 10px;
}

.block {
  background: white;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 12px 14px;
}

.title {
  font-weight: 800;
}

.wrapper {
  border: 2px dashed #111;
  border-radius: 12px;
  padding: 10px;
  background: #fff6cf;
}
  
Title
I am immediately after the title (so + .msg works).
Title with wrapper in between
I am NOT a sibling of the title. I’m inside a wrapper, so + and ~ from the title can’t reach me.
Title again
I am a later sibling of the title (so ~ .msg works).

Mini cheat sheet and best practices

Cheat sheet

  • A + B = select B immediately after A (adjacent sibling)
  • A ~ B = select all B after A (general sibling)
  • A:has(+ B) = select A if it is immediately followed by B (the “previous sibling” trick)
  • A:has(~ B) = select A if there is a later sibling B (style “everything before”)

Best practices

  • Use sibling selectors for spacing and UI relationships (like input + label) instead of adding extra utility classes everywhere.
  • Prefer + when the relationship is truly “immediately after”. It communicates intent clearly.
  • Prefer ~ when you mean “everything after this point”.
  • Reach for :has() when you genuinely need “previous/before” logic, but keep browser support in mind.

CSS Sibling Selectors Conclusion

CSS sibling selectors are powerful tools for styling elements based on their position relative to other elements. They allow you to create dynamic and context-aware styles without adding extra classes or JavaScript.

If you are just starting with CSS, also take a look at the CSS Selectors Interactive Tutorial.