More writings

The Tilde Combinator

div ~ a { … }

The ~ in CSS is called a combinator because it’s part of the group of stuff CSS offers for combining two simple selectors together. This was originally called the “indirect adjacent combinator” when it was first introduced in the early days of CSS Selectors Level 3 but as far as I can tell was renamed to “general sibling combinator” before its specification was finalized in the 2011 version of CSS Selectors Level 3. Then the latest specs Selectors Level 3 and Selectors Level 4 change the name again to the IMO much better “subsequent-sibling combinator” which I had never heard of before researching the above.

So what does it do?

This allows styling siblings that follow each other in the DOM but, unlike the + combinator, they don’t have to directly follow. It selects elements that have the same parent and follow the element selected by the left hand side of the ~ anywhere after that element within their shared parent.

Why is it odd?

The problem, for me, was that this combinator requires the element to the right of the ~ to be after the sibling described to the left of the ~. I learned this as the “general” sibling combinator which is a bit of a leaky abstraction since that name suggests it’ll select any sibling when it won’t. The “subsequent” sibling rename is pretty clear about the “this must follow in DOM” behavior but, again, this rename was new to me and I’m sure many others still have not heard of it.

For the given block of HTML:

  <label for="my-checkbox">Label</label>
  <input id="my-checkbox" type="checkbox">

If you wanted to have the label element in the DOM before the checkbox and you wanted to style the label based on the state of the checkbox you’d perhaps expect the following CSS to work:

input:checked ~ label {
    /* styles for label when input is checked */

They’re both siblings right? Yes but, you see CSS can’t look backwards and so the label has to follow the input. Anyway that’s the oddity. For what’s worth I totally sympathize with how we got this “general” (again, mitigated by renaming to “subsequent”) sibling combinator that can only look forward. I just want to surface that in light of the absolutely incredible CSS engine work that has enabled the :has() pseudo-class, my mental model for the ~ combinator never quite matched with the way it was specced, again mostly because of the old name.

Do we need a real general sibling combinator?

We obviously can’t change what ~ does because it would break existing sites. Another truer “general sibling” combinator could be introduced but it would obviously need a different name. Maybe an “any sibling” combinator? I think a new combinator would be nicer to craft into a rule than a :has() selector but would it be nice enough to be worth adding to the language?

/* with :has() */
fieldset:has(input:checked) label { … }

/* perhaps nicer? */
input:checked ~~ label { … }

Browser implementors have long been skeptical about the feasibility of :has() as it uproots longstanding optimizations in CSS parsers. Firefox in particular has not yet implemented and has many concerns. If :has() is getting significant pushback, maybe another way to do much of the same thing would be rocking the boat too much right now.